<?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: Adrien Laugueux</title>
    <description>The latest articles on DEV Community by Adrien Laugueux (@pleymor).</description>
    <link>https://dev.to/pleymor</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%2F864670%2Fdc175b83-e767-4c01-860d-b843ac2245eb.jpeg</url>
      <title>DEV Community: Adrien Laugueux</title>
      <link>https://dev.to/pleymor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pleymor"/>
    <language>en</language>
    <item>
      <title>Beyond OCR: Building a Truly Multimodal Local RAG Pipeline</title>
      <dc:creator>Adrien Laugueux</dc:creator>
      <pubDate>Wed, 11 Mar 2026 07:49:40 +0000</pubDate>
      <link>https://dev.to/pleymor/beyond-ocr-building-a-truly-multimodal-local-rag-pipeline-2hkd</link>
      <guid>https://dev.to/pleymor/beyond-ocr-building-a-truly-multimodal-local-rag-pipeline-2hkd</guid>
      <description>&lt;p&gt;If you've ever tried to build a document chatbot over a collection of scanned reports, technical manuals, or mixed-content PDFs, you've probably run into the same wall: classic RAG pipelines are essentially blind.&lt;/p&gt;

&lt;p&gt;They extract text, chunk it, embed it, and retrieve it — but the moment your document contains a scanned table, a wiring diagram, or an annotated chart, that information either gets mangled by OCR or vanishes entirely. The retrieved context is impoverished, and your chatbot's answers reflect that. Ask it about the diagram on page 12 and it will confidently summarise the paragraph next to it, which is arguably worse than saying nothing at all.&lt;/p&gt;

&lt;p&gt;There's a better way. Instead of treating documents as bags of text, you can treat them the way a human would: &lt;strong&gt;read the page as a whole&lt;/strong&gt;, visuals included. And for pages that do contain native, selectable text, you don't have to choose between precision and visual understanding — you can have both. Revolutionary, we know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OCR-Based RAG Falls Short
&lt;/h2&gt;

&lt;p&gt;The standard pipeline — OCR → chunking → embedding → vector search → LLM — was designed for text-native documents. When applied to rich, heterogeneous content, it breaks down in predictable ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A scanned table loses its structure and becomes an unreadable string of values&lt;/li&gt;
&lt;li&gt;A technical diagram is reduced to a handful of disconnected labels&lt;/li&gt;
&lt;li&gt;Spatial relationships — captions, callouts, annotations — are destroyed&lt;/li&gt;
&lt;li&gt;Charts and graphs lose all their meaning once flattened to text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The root problem is that OCR reduces a two-dimensional, semantically rich object (a page) to a one-dimensional stream of characters. You can't recover what was never captured. It's a bit like describing a painting by reading the label on the frame — technically accurate, entirely useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Idea: Combine Native Text Extraction and Vision Models
&lt;/h2&gt;

&lt;p&gt;Native text extraction and Vision Language Models (VLMs) are not competing approaches — they are complementary. Each covers what the other misses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native text&lt;/strong&gt; (via PyMuPDF) is exact, faithful to the character, and computationally free. It carries no risk of hallucination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VLMs&lt;/strong&gt; understand structure, visual semantics, and spatial relationships — things that text extraction is blind to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best pipeline uses both. On pages that contain native text alongside visual elements, native extraction handles the prose while the VLM focuses exclusively on what it does best: tables, diagrams, charts, and images. On fully scanned pages, the VLM takes over entirely.&lt;/p&gt;

&lt;p&gt;The pipeline becomes:&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%2Fsuimcwmnepwxqsqomzyv.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%2Fsuimcwmnepwxqsqomzyv.png" alt=" " width="800" height="873"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Convert Pages to Images and Extract Native Text
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fitz&lt;/span&gt;  &lt;span class="c1"&gt;# PyMuPDF
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pdf2image&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;convert_from_path&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;fitz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;page_images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convert_from_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dpi&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&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;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&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;native_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page_images&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# pass both to the processing function
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Detect Visual Elements
&lt;/h3&gt;

&lt;p&gt;PyMuPDF lets you check whether a page actually contains visuals, so you can avoid unnecessary VLM calls on text-only pages:&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;def&lt;/span&gt; &lt;span class="nf"&gt;page_has_visuals&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;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_images&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;drawings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_drawings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drawings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3 — Build a Combined Page Description
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;describe_visuals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Focus only on non-textual elements on this page:
    - Tables, diagrams, charts, images, graphs
    - Describe their content and what they convey
    - For tables, transcribe their content in Markdown or JSON
    - Ignore plain text paragraphs
    If there are no visual elements, say so briefly.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama3.2-vision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images&lt;/span&gt;&lt;span class="sh"&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;image_path&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="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;describe_full_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Describe this document page exhaustively:
    - If you see a table: transcribe its full content in a structured way
    - If you see a diagram or chart: describe its elements and relationships
    - If you see text: transcribe it faithfully
    - If you see a graph: describe the data and visible trends
    Be precise and thorough.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama3.2-vision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images&lt;/span&gt;&lt;span class="sh"&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;image_path&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="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_page&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;page_image_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;native_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&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="n"&gt;has_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;native_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="n"&gt;has_visuals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;page_has_visuals&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_text&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;has_visuals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Best of both worlds: precise text + VLM for visuals
&lt;/span&gt;        &lt;span class="n"&gt;visual_desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;describe_visuals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_image_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;## Extracted text&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;native_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;## Visual elements&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;visual_desc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;has_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Text-only page: no VLM needed
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;native_text&lt;/span&gt;

    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Fully scanned page: VLM takes over entirely
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;describe_full_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_image_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach gives you &lt;strong&gt;maximum precision on text&lt;/strong&gt; — proper nouns, exact figures, technical references — with no risk of hallucination, while the VLM adds the semantic layer that text extraction can never provide.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;process_page&lt;/code&gt; function maps directly to this decision tree: check for native text, check for visuals, and route accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Unlocks
&lt;/h2&gt;

&lt;p&gt;The combined approach improves retrieval in ways that neither method achieves alone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact recall&lt;/strong&gt;: a query for a specific article number or technical specification matches the native text verbatim&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic recall&lt;/strong&gt;: a query about "the heat flow diagram" or "the comparison table" matches the VLM's description&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structural fidelity&lt;/strong&gt;: tables are indexed as structured Markdown or JSON, not as a garbled sequence of cell values&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute efficiency&lt;/strong&gt;: the VLM only runs when there are actual visuals to describe, keeping ingestion time reasonable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Going Further: ColPali
&lt;/h2&gt;

&lt;p&gt;For the most demanding use cases, &lt;strong&gt;ColPali&lt;/strong&gt; takes a fundamentally different approach: it embeds document pages directly as images, without any intermediate text representation. Queries are embedded in the same visual space, and retrieval is based on visual similarity.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;colpali_engine.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ColPali&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ColPaliProcessor&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ColPali&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vidore/colpali-v1.2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Both page images and text queries are embedded directly
# Retrieval happens in the visual embedding space
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benefit is zero information loss — the layout itself is part of the index. ColPali consistently ranks among the best-performing models on document retrieval benchmarks, particularly for visually complex pages. It can also be combined with the hybrid approach above: use ColPali for retrieval, then pass the retrieved page image plus its extracted text to the LLM for generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Stack (Fully Local)
&lt;/h2&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%2Fzajeuvt1rfuulokhkdfo.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%2Fzajeuvt1rfuulokhkdfo.png" alt=" " width="800" height="95"&gt;&lt;/a&gt;&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.vectorstores&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chroma&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_ollama&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OllamaEmbeddings&lt;/span&gt;

&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OllamaEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nomic-embed-text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;vectorstore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Chroma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding_function&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persist_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Index each combined page description
&lt;/span&gt;&lt;span class="n"&gt;vectorstore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_texts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;page_description&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;metadatas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page_num&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;h2&gt;
  
  
  Practical Advice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Chunk at the page level.&lt;/strong&gt; A page is a natural semantic unit for a VLM. Splitting mid-page breaks the visual context the model needs to produce a coherent description.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the original images.&lt;/strong&gt; Store the source page image alongside its description. When a page is retrieved, you can pass the image directly to the LLM as additional context — especially useful for complex visuals that are hard to describe fully in text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tailor your VLM prompts to document type.&lt;/strong&gt; A technical schematic, a financial report, and a product datasheet each warrant different prompting strategies. Investing in prompt templates per document category pays off in description quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request structured output for tables.&lt;/strong&gt; When a page contains tabular data, explicitly ask the VLM to output Markdown or JSON. This preserves structure in a way that plain prose cannot, and makes the indexed content far easier for the LLM to reason over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The classic OCR-based RAG pipeline was never designed for visually rich documents. The solution isn't to replace text extraction with a VLM — it's to use both in concert. Native text gives you precision and reliability; the VLM gives you visual understanding. Together, they produce page descriptions that are richer than either could achieve alone. Think of it as hiring both a speed-reader and an art critic, and making them share a desk.&lt;/p&gt;

&lt;p&gt;Combined with a fully local stack, this approach gives you a document chatbot that can reason over tables, diagrams, charts, and mixed content, without any data leaving your infrastructure. The tooling is mature, the models are capable, and the entire pipeline runs on commodity hardware. There's no reason to settle for text-only anymore.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Bye bye Team Viewer</title>
      <dc:creator>Adrien Laugueux</dc:creator>
      <pubDate>Thu, 27 Mar 2025 13:28:08 +0000</pubDate>
      <link>https://dev.to/pleymor/bye-bye-team-viewer-4dij</link>
      <guid>https://dev.to/pleymor/bye-bye-team-viewer-4dij</guid>
      <description>&lt;p&gt;This article targets developers who'd like to replace &lt;strong&gt;Team Viewer&lt;/strong&gt; by &lt;strong&gt;RustDesk&lt;/strong&gt;, a FREE and open source solution hosted on their own server/VPN:&lt;/p&gt;

&lt;p&gt;🔐 For data privacy and security&lt;br&gt;
💰 For budget&lt;br&gt;
🚀 To have zero limits&lt;br&gt;
✨ Compliant with most of devices (windows, android, etc.)&lt;br&gt;
😎 And super easy to setup&lt;/p&gt;

&lt;p&gt;This tutorial explains step-by-step how to setup this solution, from scratch. If you already have your VPN, just skip this part 🚀.&lt;/p&gt;

&lt;p&gt;My opinion is that Team Viewer is great for individuals using the free version or for big companies who need to handle a huge parc of machines.&lt;/p&gt;

&lt;p&gt;But for smaller companies, I definitely suggest RustDesk.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites of this tutorial
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A device with a terminal and &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;scp&lt;/code&gt; installed&lt;/li&gt;
&lt;li&gt;A RustDesk client installed on every device (controller or to be controlled). Download from &lt;a href="https://github.com/rustdesk/rustdesk/releases" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;This tutorial assumes you get a server under &lt;strong&gt;Debian&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  First step: subscribing a VPS plan
&lt;/h2&gt;

&lt;p&gt;There are many VPS providers. I personally chose the german Netcup for its competitive prices, and for the absence of commitment (I can stop when I want).&lt;/p&gt;

&lt;p&gt;Here is the subscription page: &lt;a href="https://www.netcup.eu/vserver/vps.php" rel="noopener noreferrer"&gt;https://www.netcup.eu/vserver/vps.php&lt;/a&gt;&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%2F7o8197v9ryakbmi2yzu5.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%2F7o8197v9ryakbmi2yzu5.png" alt="Netcup VPS current prices" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;VPS 200 G11s is enough for most of people. It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Processor: 2 vCore (x86)&lt;/li&gt;
&lt;li&gt;Main memory: 2 GB&lt;/li&gt;
&lt;li&gt;Hard disk: 64 GB SSD&lt;/li&gt;
&lt;li&gt;Minimum availability: 96.6%&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Second step: admin access
&lt;/h2&gt;

&lt;p&gt;Once the order is maid (yes, there is no payment step), a confirmation email will be sent, followed by a few more a few minutes later.&lt;/p&gt;

&lt;p&gt;The most important mail will contain  your customer number and password, to access the administration part:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;we are pleased to welcome you as a customer at netcup. Enclosed you will find your access data to the netcup CCP (customer control panel).&lt;br&gt;
There you have the possibility to maintain your data and products, as well as to view past invoices.&lt;br&gt;
Your access data to the CCP are as follows:&lt;br&gt;
Customer number: 123456&lt;br&gt;
Password: blablabla&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So don't forget to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your customer number (only digits)&lt;/li&gt;
&lt;li&gt;your password&lt;/li&gt;
&lt;li&gt;the link to administrate Netcup VPS: &lt;a href="https://www.customercontrolpanel.de/rechnungen.php" rel="noopener noreferrer"&gt;https://www.customercontrolpanel.de/rechnungen.php&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the link to technically administrate the VPS: &lt;a href="https://www.servercontrolpanel.de/SCP/Home" rel="noopener noreferrer"&gt;https://www.servercontrolpanel.de/SCP/Home&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the ip of your VPS&lt;/li&gt;
&lt;li&gt;the initial root password (to be changed in the next part)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Third step: root password configuration
&lt;/h2&gt;

&lt;p&gt;First, for security reason, the root password must be changed via the admin interface. Access it via: &lt;a href="https://www.customercontrolpanel.de/rechnungen.php" rel="noopener noreferrer"&gt;https://www.customercontrolpanel.de/rechnungen.php&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see our VPS listed, in the &lt;code&gt;Products&lt;/code&gt; section:&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%2Fujju65wql18lpcb3oqyo.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%2Fujju65wql18lpcb3oqyo.png" alt="Netcup list of VPS" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then by clicking on the 🔎, the URL to admin panel is displayed:&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%2Fs90wphhq9gccb6chimd5.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%2Fs90wphhq9gccb6chimd5.png" alt="Netcup admin panel" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's always &lt;a href="https://www.servercontrolpanel.de/SCP/Home" rel="noopener noreferrer"&gt;https://www.servercontrolpanel.de/SCP/Home&lt;/a&gt; but just in case you lose it, this way you know where you can find it.&lt;/p&gt;

&lt;p&gt;Once there, we can start the configuration of the server (it's very fast):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;General &amp;gt; give a nickname to your server (optional but i recommend it)
&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%2Fby4au6g47m93q29bnbyj.png" alt="Configuration / General" width="586" height="43"&gt;
&lt;/li&gt;
&lt;li&gt;Control &amp;gt; click on Shutdown (ACPI). required to set a new root password&lt;/li&gt;
&lt;li&gt;Access &amp;gt; change the root password and NOTE IT&lt;/li&gt;
&lt;li&gt;Control &amp;gt; restart the server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ Do not even try to use the terminal available on the &lt;code&gt;General&lt;/code&gt; tab, you'll probably never be able to simply enter your password, because of the poor keymap support. To access it we'll open a real SSH terminal.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fourth step: ssh access
&lt;/h2&gt;

&lt;p&gt;Open your favorite terminal with ssh installed and run this command (of course adapt it to your server).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@YOUR_IP
&lt;span class="c"&gt;# To the question "Are you sure you want to continue connecting (yes/no/[fingerprint])?", press enter&lt;/span&gt;
Then enter the root password you created &lt;span class="k"&gt;in &lt;/span&gt;the &lt;span class="s2"&gt;"Third step"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fifth step: user creation
&lt;/h2&gt;

&lt;p&gt;For security reasons (if you are interested you can check &lt;a href="https://superuser.com/questions/666942/why-it-is-not-recommend-to-use-root-login-in-linux#:~:text=The%20primary%20reasons%20are%20thus,account%20should%20have%20logins%20disabled." rel="noopener noreferrer"&gt;here&lt;/a&gt;), it is recommended to run your programs as non-root user.&lt;/p&gt;

&lt;p&gt;Here is how to create a user named "admin" (feel free to set the name of your choice of course):&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="c"&gt;# user creation&lt;/span&gt;
useradd admin

&lt;span class="c"&gt;# password creation&lt;/span&gt;
passwd admin
&lt;span class="c"&gt;# here create a password for admin&lt;/span&gt;

&lt;span class="c"&gt;# set user as sudoer&lt;/span&gt;
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;admin

&lt;span class="c"&gt;# Create his home directory&lt;/span&gt;
mkhomedir_helper admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sixth step: RustDesk server installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# use your non-root user&lt;/span&gt;
su - admin

&lt;span class="c"&gt;# install ufw&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ufw

&lt;span class="c"&gt;# open ports&lt;/span&gt;
ufw allow proto tcp from YOURIP to any port 22
ufw allow 21114:21119/tcp
ufw allow 8000/tcp
ufw allow 21116/udp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;

&lt;span class="c"&gt;# download and run the installer&lt;/span&gt;
wget https://raw.githubusercontent.com/techahold/rustdeskinstall/master/install.sh
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x install.sh
./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: if you had the good taste to have a domain linked to your VPS IP, you can enter it when requested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Note well the public key, you'll need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seventh step: Install RustDesk on the host (computer to be controlled)
&lt;/h2&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%2F865g7rlczxogy87kjq9j.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%2F865g7rlczxogy87kjq9j.png" alt="Frodon and the ring" width="777" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Execute the exe, it should open this window:&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%2F55zggkgrrf12wtaiy8re.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%2F55zggkgrrf12wtaiy8re.png" alt="RustDesk client" width="757" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on Install in the pink button on the left.&lt;/p&gt;

&lt;p&gt;Open the settings:&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%2F2rs92v9jf1vw1ko7furg.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%2F2rs92v9jf1vw1ko7furg.png" alt="RustDesk settings" width="799" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Only enter the &lt;strong&gt;ID Server&lt;/strong&gt; with the IP of your VPS, or the domain name if you have one and if configured during installation.&lt;/p&gt;

&lt;p&gt;You should now see "Ready" written in green in the bottom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My advice&lt;/strong&gt;: for simplicity, set a permanent password in the security settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Last step: Install RustDesk on the client (computer to rule them all)
&lt;/h2&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%2Frykp5ck7duk1fo57vqg6.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%2Frykp5ck7duk1fo57vqg6.png" alt="Sauron tower" width="701" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same installation, excepted that in the Network settings you'll have to fill &lt;strong&gt;Key&lt;/strong&gt; with the public key I told you to save in the "Sixth step".&lt;/p&gt;

&lt;p&gt;Now add the ID and password (the one you set if you followed my advice, otherwise the rotating one).&lt;/p&gt;

&lt;p&gt;And you are good, now you can access your device by double-clicking on it and configure as many devices as you want!&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful links
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rustdesk.com/docs/en/self-host/" rel="noopener noreferrer"&gt;RustDesk self-hosting doc&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.netcup.eu/vserver/vps.php" rel="noopener noreferrer"&gt;Netcup VPS&lt;/a&gt; and &lt;a href="https://contabo.com/en/vps/" rel="noopener noreferrer"&gt;Contabo VPS&lt;/a&gt; which are my favorite ones&lt;/p&gt;

&lt;p&gt;👋 Thanks for reading, and feel free to share your feelings!&lt;/p&gt;

</description>
      <category>teamviewer</category>
      <category>rustdesk</category>
      <category>remotedesktop</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I created my personal VPN with static IP</title>
      <dc:creator>Adrien Laugueux</dc:creator>
      <pubDate>Fri, 17 Mar 2023 19:36:00 +0000</pubDate>
      <link>https://dev.to/pleymor/how-i-created-my-personal-vpn-with-fixed-ip-1f43</link>
      <guid>https://dev.to/pleymor/how-i-created-my-personal-vpn-with-fixed-ip-1f43</guid>
      <description>&lt;p&gt;This article targets developers who'd need their VPN:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For confidentiality (especially when working with public wifi)&lt;/li&gt;
&lt;li&gt;If company resources are restricted to IP whitelists&lt;/li&gt;
&lt;li&gt;For any other reason&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have a good reading =)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why using a VPS to create a VPN
&lt;/h2&gt;

&lt;p&gt;I identified 3 main ways to get a vpn with a static IP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💰💰 Subscribe a VPN with static IP option. This option can be great if beyond the fixed IP and confidentiality aspects, you want to be able to be behind an IP of the country of your choice. It's the easiest and most powerful way, but... also the most expensive. Note that static IPs are only available in paying VPN (at least today, in March 2023), so normally they don't make money with your personal data, like some free VPN.&lt;/li&gt;
&lt;li&gt;🧔 Install your own personal VPN server at home, like on your favorite Raspberry pie zero. Given the consumption of such a device, it's very probably the least expensive way. But you are limited to your up bandwidth, so if you're not fibered, you'll feel it.&lt;/li&gt;
&lt;li&gt;❤️ What I chose and what I'll present here is installing OpenVPN on a VPS (Virtual Private Server). It's a bit more technical, but very fast if you have the right guidelines. I prefer this solution to the 2 other ones, because small VPS are enough and clearly less expensive than VPN, because I'm not fibered and because it's more reliable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A device with a terminal and &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;scp&lt;/code&gt; installed&lt;/li&gt;
&lt;li&gt;An OpenVPN client installed on the machine you want to use a vpn.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  First step: subscribing a VPS plan
&lt;/h2&gt;

&lt;p&gt;There are many VPS providers. I personally chose the german Netcup for its competitive prices, and for the absence of commitment (I can stop when I want).&lt;/p&gt;

&lt;p&gt;Here is the subscription page: &lt;a href="https://www.netcup.eu/vserver/vps.php" rel="noopener noreferrer"&gt;https://www.netcup.eu/vserver/vps.php&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fknhti2u43w0jve7lzrki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fknhti2u43w0jve7lzrki.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;VPS 200 G10s is clearly enough to make our VPN. It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Processor: 2 vCore&lt;/li&gt;
&lt;li&gt;Main memory: 2 GB&lt;/li&gt;
&lt;li&gt;Hard disk: 40 GB SSD (RAID10)&lt;/li&gt;
&lt;li&gt;Unthrottled traffic: 80 TB / month (80 000 GB per month!)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Second step: admin access
&lt;/h2&gt;

&lt;p&gt;Once the order is maid (yes, there is no payment step), a confirmation email will be sent, followed by a few more a few minutes later.&lt;/p&gt;

&lt;p&gt;The most important mail will contain  your customer number and password, to access the administration part:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;we are pleased to welcome you as a customer at netcup. Enclosed you will find your access data to the netcup CCP (customer control panel).&lt;br&gt;
There you have the possibility to maintain your data and products, as well as to view past invoices.&lt;br&gt;
Your access data to the CCP are as follows:&lt;br&gt;
Customer number: 123456&lt;br&gt;
Password: blablabla&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So don't forget to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your customer number (only digits)&lt;/li&gt;
&lt;li&gt;your password&lt;/li&gt;
&lt;li&gt;the link to administrate Netcup VPS: &lt;a href="https://www.customercontrolpanel.de/rechnungen.php" rel="noopener noreferrer"&gt;https://www.customercontrolpanel.de/rechnungen.php&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the link to technically administrate the VPS: &lt;a href="https://www.servercontrolpanel.de/SCP/Home" rel="noopener noreferrer"&gt;https://www.servercontrolpanel.de/SCP/Home&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the ip of your VPS&lt;/li&gt;
&lt;li&gt;the initial root password (to be changed in the next part)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Third step: root password configuration
&lt;/h2&gt;

&lt;p&gt;First, for security reason, the root password must be changed via the admin interface. Access it via: &lt;a href="https://www.customercontrolpanel.de/rechnungen.php" rel="noopener noreferrer"&gt;https://www.customercontrolpanel.de/rechnungen.php&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see our VPS listed, in the &lt;code&gt;Products&lt;/code&gt; section:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fujju65wql18lpcb3oqyo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fujju65wql18lpcb3oqyo.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then by clicking on the 🔎, the URL to admin panel is displayed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fs90wphhq9gccb6chimd5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fs90wphhq9gccb6chimd5.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's always &lt;a href="https://www.servercontrolpanel.de/SCP/Home" rel="noopener noreferrer"&gt;https://www.servercontrolpanel.de/SCP/Home&lt;/a&gt; but just in case you lose it, this way you know where you can find it.&lt;/p&gt;

&lt;p&gt;Once there, we can start the configuration of the server (it's very fast):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;General &amp;gt; give a nickname to your server (optional but i recommend it)
&lt;img src="https://media.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%2Fby4au6g47m93q29bnbyj.png" alt="Image description"&gt;
&lt;/li&gt;
&lt;li&gt;Control &amp;gt; click on Shutdown (ACPI). required to set a new root password&lt;/li&gt;
&lt;li&gt;Access &amp;gt; change the root password and NOTE IT&lt;/li&gt;
&lt;li&gt;Control &amp;gt; restart the server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ Do not even try to use the terminal available on the &lt;code&gt;General&lt;/code&gt; tab, you'll probably never be able to simply enter your password, because of the poor keymap support. To access it we'll open a real SSH terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fourth step: ssh access
&lt;/h2&gt;

&lt;p&gt;Open your favorite terminal with ssh installed and run this command (of course adapt it to your server).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@YOUR_IP
&lt;span class="c"&gt;# To the question "Are you sure you want to continue connecting (yes/no/[fingerprint])?", press enter&lt;/span&gt;
Then enter the root password you created &lt;span class="k"&gt;in &lt;/span&gt;the &lt;span class="s2"&gt;"Third step"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fifth step: user creation
&lt;/h2&gt;

&lt;p&gt;For security reasons (if you are interested you can check &lt;a href="https://superuser.com/questions/666942/why-it-is-not-recommend-to-use-root-login-in-linux#:~:text=The%20primary%20reasons%20are%20thus,account%20should%20have%20logins%20disabled." rel="noopener noreferrer"&gt;here&lt;/a&gt;), it is recommended to run your programs as non-root user.&lt;/p&gt;

&lt;p&gt;Here is how to create a user named "admin" (feel free to set the name of your choice of course):&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="c"&gt;# user creation&lt;/span&gt;
useradd admin

&lt;span class="c"&gt;# password creation&lt;/span&gt;
passwd admin
&lt;span class="c"&gt;# here create a password for admin&lt;/span&gt;

&lt;span class="c"&gt;# set user as sudoer&lt;/span&gt;
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;admin

&lt;span class="c"&gt;# Create his home directory&lt;/span&gt;
mkhomedir_helper admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sixth step: OpenVPN installation
&lt;/h2&gt;

&lt;p&gt;For security reasons (if you are interested you can check &lt;a href="https://superuser.com/questions/666942/why-it-is-not-recommend-to-use-root-login-in-linux#:~:text=The%20primary%20reasons%20are%20thus,account%20should%20have%20logins%20disabled." rel="noopener noreferrer"&gt;here&lt;/a&gt;), it is recommended to run your programs as non-root user.&lt;/p&gt;

&lt;p&gt;Here is how to create a user named "admin" (feel free to set the name of your choice of course):&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="c"&gt;# use your non-root user&lt;/span&gt;
su - admin

&lt;span class="c"&gt;# download OpenVPN installer&lt;/span&gt;
wget https://raw.githubusercontent.com/Angristan/openvpn-install/master/openvpn-install.sh &lt;span class="nt"&gt;-O&lt;/span&gt; debian-11-vpn-server.sh

&lt;span class="c"&gt;# make it executable&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; +x debian-11-vpn-server.sh

&lt;span class="c"&gt;# execute it&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./debian-11-vpn-server.sh

&lt;span class="c"&gt;# Then simply press enter multiple times until you get this message: &lt;/span&gt;
&lt;span class="c"&gt;# "The configuration file has been written to /root/mydesktopclient.ovpn.&lt;/span&gt;
&lt;span class="c"&gt;# Download the .ovpn file and import it in your OpenVPN client."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Seventh and ultimate step: Connection of OpenVPN client
&lt;/h2&gt;

&lt;p&gt;Exit the server (&lt;code&gt;exit&lt;/code&gt; command multiple times), then let's use &lt;code&gt;scp&lt;/code&gt; command to retrieve &lt;code&gt;mydesktopclient.ovpn&lt;/code&gt; file on your PC/mac/whatever:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp admin@YOUR_IP:/home/admin/mydesktopclient.ovpn &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="c"&gt;# enter here the password you created for admin (not root, just in case)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open your OpenVPN client (install it if you don't have it yet), select the &lt;code&gt;File&lt;/code&gt; tab &amp;gt; Browse &amp;gt; select your &lt;code&gt;mydesktopclient.ovpn&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F20k36iy2pbd8vikf9kbn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F20k36iy2pbd8vikf9kbn.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Confirm by clicking on &lt;code&gt;Connect&lt;/code&gt;, and 🎉 we are done!! 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  To create a new client .ovpn file
&lt;/h2&gt;

&lt;p&gt;If you want to share your vpn with someone, instead of sharing your .ovpn file you can create a new one easily:&lt;/p&gt;

&lt;p&gt;Connect to the server with ssh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh admin@YOUR_IP
&lt;span class="c"&gt;# admin password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And run the installer:&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; ./debian-11-vpn-server.sh
&lt;span class="c"&gt;# admin password&lt;/span&gt;

&lt;span class="c"&gt;# What do you want to do?&lt;/span&gt;
1

&lt;span class="c"&gt;# Tell me a name for the client.&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; enter here a name matching this new client&lt;/span&gt;

&lt;span class="c"&gt;# Do you want to protect the configuration file with a password?&lt;/span&gt;
1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then proceed like in the "Seventh step" to retrieve the file, and share it.&lt;/p&gt;

&lt;p&gt;👋 Thanks for reading&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
