<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Jens Willmer</title>
    <description>In this blog I write about my projects and everything else that comes to my mind.</description>
    <link>https://jwillmer.de/</link>
    <atom:link href="https://jwillmer.de/feed.xml" rel="self" type="application/rss+xml"/>
      <pubDate>Thu, 02 Jul 2026 21:17:47 +0000</pubDate>
    <lastBuildDate>Thu, 02 Jul 2026 21:17:47 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Data Cleaning for RAG Search and Response</title>
        <description>&lt;p&gt;In a &lt;a href=&quot;/blog/tutorial/retrieval-augmented-generation&quot;&gt;previous post&lt;/a&gt;, I covered what Retrieval-Augmented Generation is and how to prepare data for ingestion. A &lt;a href=&quot;/blog/tutorial/data-cleaning-for-rag-ingest-pipeline&quot;&gt;companion post on the ingest pipeline&lt;/a&gt; walks through the data cleaning techniques that get content into the vector store. This post picks up where retrieval begins.&lt;/p&gt;

&lt;p&gt;Ingesting documents into a vector database is only half the problem. The other half is what happens when someone types a question: understanding the query, ranking results, validating citations, and handling failures along the way.&lt;/p&gt;

&lt;h2 id=&quot;query-handling&quot;&gt;Query handling&lt;/h2&gt;

&lt;p&gt;User input can contain control characters, excessively long text, or prompt injection attempts. Before the query reaches any LLM or embedding model, three layers of sanitization run:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Control character removal: strip everything except newlines and tabs.&lt;/li&gt;
  &lt;li&gt;Prompt injection mitigation: regex-based detection of patterns like “ignore previous instructions” or “disregard system prompt.” Matched patterns are stripped before the query reaches any model.&lt;/li&gt;
  &lt;li&gt;Length truncation: queries are capped at 500 characters for classification and topic extraction (4000 for full content extraction).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even though the LLM system prompts include their own guardrails, sanitizing at the boundary prevents entire categories of attacks from reaching the model.&lt;/p&gt;

&lt;h2 id=&quot;intent-classification&quot;&gt;Intent classification&lt;/h2&gt;

&lt;p&gt;Once the query is clean, the next question is: does it even need the RAG pipeline?&lt;/p&gt;

&lt;p&gt;A four-way intent classifier runs before any retrieval:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Intent&lt;/th&gt;
      &lt;th&gt;Action&lt;/th&gt;
      &lt;th&gt;Example&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;factual_query&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Full RAG pipeline&lt;/td&gt;
      &lt;td&gt;“What happened to vessel X in January?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;exploratory&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Skip RAG, offer clarification&lt;/td&gt;
      &lt;td&gt;“I’m wondering about maintenance schedules…”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;off_topic&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Skip RAG, scope reminder&lt;/td&gt;
      &lt;td&gt;“What’s the weather in Athens?”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;greeting&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Canned response, no API calls&lt;/td&gt;
      &lt;td&gt;“Hi”, “Thanks”&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;A regex fast-path catches common greetings before the LLM classifier runs. The classifier uses temperature=0.0 for deterministic routing, so identical queries always get the same intent. If the classifier fails entirely, the system defaults to &lt;code&gt;factual_query&lt;/code&gt; so search is never blocked by a transient error.&lt;/p&gt;

&lt;p&gt;In typical usage, 15-25% of messages are greetings or acknowledgments. Routing them correctly saves 200-400ms and $0.001-0.01 per message by skipping embedding generation, vector search, and reranking.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;search-and-retrieval&quot;&gt;Search and retrieval&lt;/h2&gt;

&lt;p&gt;With a confirmed &lt;code&gt;factual_query&lt;/code&gt;, the system needs to find relevant content. This happens in several coordinated steps.&lt;/p&gt;

&lt;h3 id=&quot;topic-based-filtering&quot;&gt;Topic-based filtering&lt;/h3&gt;

&lt;p&gt;Searching the entire vector space for every query produces noisy results. A question about cargo damage should not return chunks about engine maintenance just because they share vocabulary.&lt;/p&gt;

&lt;p&gt;Topics are extracted from the query and matched against the corpus topic ontology using cosine similarity. The default mode is embedding-only: the query embedding is matched directly against topic embeddings, skipping an LLM extraction step and saving ~300ms per query.&lt;/p&gt;

&lt;p&gt;Because the topic ontology is fragmented (median chunk count per topic is 1, concepts span ~7 synonymous topic rows), the matcher returns top-K closest topics per name rather than a single winner, pooling fragments into effective clusters.&lt;/p&gt;

&lt;p&gt;If topic-filtered results return fewer chunks than a threshold, the filter retries with a looser similarity threshold. If still too sparse, the topic filter is dropped entirely while structural filters (like vessel metadata) are preserved.&lt;/p&gt;

&lt;h3 id=&quot;vessel-metadata-filtering&quot;&gt;Vessel metadata filtering&lt;/h3&gt;

&lt;p&gt;Fleet operators frequently need results for a specific vessel, type, or class. These are structural metadata filters applied at the database level before vector similarity: exact, not probabilistic. When an operator asks about a specific vessel, they get results only from that vessel.&lt;/p&gt;

&lt;p&gt;Vessel names are resolved with fuzzy matching against a cached vessel list, and whole-word regex auto-detects vessel types and classes mentioned in the query text. Active filters persist per conversation, so follow-up questions inherit vessel context.&lt;/p&gt;

&lt;h3 id=&quot;hybrid-vector--bm25-search&quot;&gt;Hybrid vector + BM25 search&lt;/h3&gt;

&lt;p&gt;Pure vector similarity misses exact keyword matches (part numbers, IMO numbers, incident codes). Pure keyword search misses semantic meaning. The system combines both:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Vector search via HNSW index (pgvector) with cosine similarity. &lt;code&gt;hnsw.ef_search = 100&lt;/code&gt; (up from the default 40) trades a few milliseconds of latency for 5-10% recall improvement.&lt;/li&gt;
  &lt;li&gt;Optional BM25 scoring boosts results that match query keywords exactly.&lt;/li&gt;
  &lt;li&gt;A similarity threshold (default 0.3) discards low-confidence results before reranking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This catches both semantic matches (“propulsion system failure” matching “engine breakdown”) and exact matches (“IMO 9876543” matching only that specific number).&lt;/p&gt;

&lt;h3 id=&quot;thread-digest-retrieval&quot;&gt;Thread digest retrieval&lt;/h3&gt;

&lt;p&gt;Broad queries like “any engine issues last month?” need to match content spanning an entire email thread. Individual message chunks may contain only the problem or only the resolution.&lt;/p&gt;

&lt;p&gt;During ingest, multi-message threads get a synthesized digest chunk (200-400 words) that captures the full problem-investigation-resolution arc. At search time, this digest competes alongside regular message chunks in vector similarity. A query like “how was the cylinder crack resolved?” might match the resolution message chunk poorly (it mentions “replaced liner” without context), but the digest chunk matches well because it contains the full narrative.&lt;/p&gt;

&lt;h2 id=&quot;reranking&quot;&gt;Reranking&lt;/h2&gt;

&lt;p&gt;After initial vector retrieval returns ~40 candidates, a cross-encoder model (Cohere rerank) scores each candidate against the query. Cross-encoders process the query and document together rather than encoding them separately, and are 20-35% more accurate than bi-encoder similarity alone.&lt;/p&gt;

&lt;p&gt;Before reranking, each document is prefixed with its email subject and source title. This helps the reranker distinguish between chunks from different incidents that have similar technical content.&lt;/p&gt;

&lt;p&gt;Results below a rerank score of 0.2 are filtered out, but at least one result is always returned to prevent empty responses. If the reranking API fails, the pipeline falls back to unranked vector results with a warning log.&lt;/p&gt;

&lt;p&gt;In practice, reranking made the biggest difference to answer quality. The LLM attends most to the first chunks in its context, so getting the order right matters more than retrieving a few extra candidates.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;building-the-llm-context&quot;&gt;Building the LLM context&lt;/h2&gt;

&lt;p&gt;At ingest time, each attachment is classified into one of three embedding modes (&lt;code&gt;full&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt;, or &lt;code&gt;metadata_only&lt;/code&gt;). This classification carries through to how context is built for the LLM.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;full&lt;/code&gt; mode, a context summary is prepended to the chunk text, separated by &lt;code&gt;---&lt;/code&gt;. The LLM sees both document-level context and the specific passage. In &lt;code&gt;summary&lt;/code&gt; mode, only the context summary is emitted, since the chunk content is already a synthesized summary and repeating it wastes context window. &lt;code&gt;metadata_only&lt;/code&gt; emits just the filename and basic metadata.&lt;/p&gt;

&lt;p&gt;Each search result also gets a structured citation header:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[CITE:chunk_id | title:&quot;Cargo Report...&quot; | subject:&quot;RE: Vessel Inspection...&quot; | date:2024-01-15 | from:captain@... | page:5]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Only the chunk ID is exposed as an identifier, no doc_id or source_id to confuse the model. Title is capped at 50 characters, subject at 40.&lt;/p&gt;

&lt;p&gt;When multiple results come from the same email thread, they are grouped by root email with an incident summary. The LLM sees “3 incidents found, 12 relevant passages total” rather than a flat list, so it can piece together the timeline of an incident from multiple chunks.&lt;/p&gt;

&lt;h2 id=&quot;citations-and-trust&quot;&gt;Citations and trust&lt;/h2&gt;

&lt;p&gt;The LLM generates inline &lt;code&gt;[C:chunk_id]&lt;/code&gt; markers, which then go through a validation pipeline:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Regex extraction captures citation markers (tolerating whitespace variations).&lt;/li&gt;
  &lt;li&gt;ID validation confirms each chunk ID is exactly 12 hex characters.&lt;/li&gt;
  &lt;li&gt;Deduplication collapses repeated citations of the same chunk.&lt;/li&gt;
  &lt;li&gt;Archive verification concurrently checks that each referenced file exists in storage, running in parallel so verification takes max(times) instead of sum.&lt;/li&gt;
  &lt;li&gt;If more than 50% of citations are invalid, the response is flagged. Invalid markers are stripped from the response text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the frontend, citations render as numbered badges with hover previews. When the &lt;code&gt;data-citations&lt;/code&gt; SSE frame arrives mid-stream, raw &lt;code&gt;[C:hex]&lt;/code&gt; markers are replaced with full HTML tags containing metadata, source title, page number, and download URI. A 3-second fallback ensures citations still render even if the validation pipeline is slow.&lt;/p&gt;

&lt;p&gt;Users can click a citation badge to see the original email, the highlighted passage, and download the original PDF. Citation detail endpoints validate chunk IDs at the API boundary (12-char hex format check) before any database query runs.&lt;/p&gt;

&lt;p&gt;For stored conversations, the citation payload is persisted alongside each assistant message. When a user returns days later, the same rendering function produces identical results: working links, accurate source references, same visual treatment.&lt;/p&gt;

&lt;h2 id=&quot;reliability&quot;&gt;Reliability&lt;/h2&gt;

&lt;p&gt;The search pipeline depends on several external services: embedding API, vector database, reranking API, LLM provider. Any of them can fail transiently. Every stage has a fallback:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Stage&lt;/th&gt;
      &lt;th&gt;On failure&lt;/th&gt;
      &lt;th&gt;Fallback&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Intent classifier&lt;/td&gt;
      &lt;td&gt;LLM error&lt;/td&gt;
      &lt;td&gt;Default to &lt;code&gt;factual_query&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Topic extraction&lt;/td&gt;
      &lt;td&gt;LLM error&lt;/td&gt;
      &lt;td&gt;Broad search (no topic filter)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Topic filter&lt;/td&gt;
      &lt;td&gt;Too few results&lt;/td&gt;
      &lt;td&gt;Retry with loose threshold, then drop filter&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Reranking&lt;/td&gt;
      &lt;td&gt;API error&lt;/td&gt;
      &lt;td&gt;Return unranked vector results&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Citation validation&lt;/td&gt;
      &lt;td&gt;Archive check fails&lt;/td&gt;
      &lt;td&gt;Mark as unverified (still renders)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Citation SSE frame&lt;/td&gt;
      &lt;td&gt;Network timeout&lt;/td&gt;
      &lt;td&gt;Frontend renders from raw markers&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Error messages that reach the user are sanitized: Bearer tokens, API keys, and database connection strings are stripped before display.&lt;/p&gt;

&lt;p&gt;LangGraph agent checkpoints can also contain interrupted tool calls (the LLM requested a search, but the response was never received). Before each LLM call, the message history is scanned and orphaned calls are filtered out. Without this, a single interrupted request would make the entire conversation unusable.&lt;/p&gt;

&lt;p&gt;Prompt caching reduces latency on follow-up messages by 40-60%. The system prompt is marked as a stable prefix for caching; dynamic content (vessel context, intent-specific instructions) is placed after conversation history so it does not invalidate the cache.&lt;/p&gt;

&lt;h2 id=&quot;observability&quot;&gt;Observability&lt;/h2&gt;

&lt;p&gt;Without visibility into search quality, degradation goes unnoticed. Bad results do not produce errors; they produce silent user dissatisfaction.&lt;/p&gt;

&lt;p&gt;Every search pipeline step is traced with Langfuse: session ID, user ID, intent classification result, topic filter decisions, retrieval count, rerank scores, generation output, and citation validation outcome. Users can submit thumbs-up/thumbs-down on each response, recorded as a Langfuse score linked to the specific trace.&lt;/p&gt;

&lt;p&gt;Low-rated responses can be investigated by examining the full pipeline trace: which topics were extracted, which chunks were retrieved, what the reranker promoted, and what the LLM generated. If you do not have this kind of tracing, quality issues stay invisible until someone complains.&lt;/p&gt;

&lt;p&gt;Citation detail endpoints validate chunk IDs before querying the database. Archive file serving rejects path traversal attempts. All endpoints have per-user rate limits (30/min for search, 100/min for citations) to protect upstream API budgets. Download URLs use pre-signed links with 300-second expiry rather than open storage access.&lt;/p&gt;

&lt;h2 id=&quot;what-held-up&quot;&gt;What held up&lt;/h2&gt;

&lt;p&gt;Reranking was the biggest quality improvement: a cross-encoder examining query and document together was 20-35% more accurate than embedding similarity alone. Topic filtering and metadata filters improved precision without sacrificing recall, because the loosening fallback catches edge cases.&lt;/p&gt;

&lt;p&gt;I did not expect citation validation to matter as much as it did. A few dead links in an otherwise good answer made users distrust the whole system. Verifying every citation against the source archive before it reaches the user is worth the added latency.&lt;/p&gt;

&lt;p&gt;The ingest pipeline gets data in. The search pipeline gets the right data out. Both need the same care around data quality, just at different points, and on the search side the user is watching in real time.&lt;/p&gt;
</description>
        <pubDate>Tue, 23 Jun 2026 08:00:00 +0000</pubDate>
        <link>https://jwillmer.de/blog/tutorial/data-cleaning-for-rag-search-and-response</link>
        <guid isPermaLink="true">https://jwillmer.de/blog/tutorial/data-cleaning-for-rag-search-and-response</guid>
        
        <category>rag</category>
        
        <category>search</category>
        
        <category>reranking</category>
        
        <category>citations</category>
        
        <category>llm</category>
        
        <category>vector-search</category>
        
        <category>semantic-search</category>
        
        <category>query-processing</category>
        
        
        <category>Tutorial</category>
        
      </item>
    
      <item>
        <title>Data Cleaning for a RAG Ingest Pipeline</title>
        <description>&lt;p&gt;If your RAG pipeline ingests dirty data, the answers will be wrong. The embedding model and prompt chain cannot fix what was broken before indexing.&lt;/p&gt;

&lt;p&gt;I built this pipeline for a maritime email corpus: thousands of &lt;code&gt;.eml&lt;/code&gt; files with PDF attachments, Office documents, images, and ZIP archives, turned into a searchable knowledge base. The examples here are maritime, but the patterns apply to any industry where you ingest unstructured documents into a RAG system. Corporate email, support tickets, compliance archives, internal wikis: the same cleaning problems show up everywhere.&lt;/p&gt;

&lt;p&gt;This post covers the techniques that actually mattered, based on a corpus of 6,000+ emails with tens of thousands of attachments.&lt;/p&gt;

&lt;h2 id=&quot;cleaning-the-raw-email-content&quot;&gt;Cleaning the raw email content&lt;/h2&gt;

&lt;p&gt;Corporate email is one of the noisiest data sources you can feed into a RAG system. A single email thread might contain the incident report you actually care about, buried under forwarded headers, legal disclaimers, satellite communication blocks, &lt;code&gt;mailto:&lt;/code&gt; links, and five previous replies where someone wrote “OK, noted.”&lt;/p&gt;

&lt;p&gt;The first step is thread splitting. Regex patterns detect message boundaries across Gmail, Outlook, and generic separators, splitting a forwarded thread into individual messages so each can be cleaned independently. Over 20 boilerplate regex patterns then strip &lt;code&gt;&amp;lt;mailto:&amp;gt;&lt;/code&gt; links, &lt;code&gt;[cid:]&lt;/code&gt; image references, horizontal separator lines, phone/fax contact blocks, satellite communication headers (common in maritime), address blocks, legal disclaimers, and unsubscribe footers.&lt;/p&gt;

&lt;p&gt;For the edges that regex cannot handle cleanly, an LLM-based boundary detector identifies the first and last meaningful words in the email body, trimming headers and signatures with fuzzy matching that tolerates formatting drift between email clients.&lt;/p&gt;

&lt;p&gt;Messages under 20 words get discarded entirely. Auto-replies and bare acknowledgments like “Noted, thanks” produce low-value chunks that score artificially high once context enrichment is applied, pushing the actual incident descriptions out of search results.&lt;/p&gt;

&lt;p&gt;Without this cleanup, embeddings cluster around disclaimer text instead of the actual content. A query about “main engine cylinder crack” should not return ten results whose top match is a legal footer that appears in every email.&lt;/p&gt;

&lt;h2 id=&quot;encoding-html-and-character-corruption&quot;&gt;Encoding, HTML, and character corruption&lt;/h2&gt;

&lt;p&gt;Emails arrive from systems worldwide using different character encodings. A single encoding error can cascade: a garbled MIME header prevents attachment extraction, and you lose an entire document from the index.&lt;/p&gt;

&lt;p&gt;The pipeline uses a cascading charset fallback that tries encodings in priority order: declared charset, UTF-8, UTF-8-sig (with BOM), ISO-8859-1, Windows-1252, CP1252. Only as a last resort does it use UTF-8 with replacement characters. UTF-8 BOM bytes are stripped before parsing, and CSV files from legacy systems are truncated at the first NUL byte with line endings normalized.&lt;/p&gt;

&lt;p&gt;HTML-to-text conversion strips &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tags, converts &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tags to markdown links, replaces block elements with newlines, and unescapes HTML entities. Plain text MIME parts are preferred when available, because clean plain text produces better embeddings than HTML-tagged content.&lt;/p&gt;

&lt;h2 id=&quot;parsing-routing-by-complexity&quot;&gt;Parsing: routing by complexity&lt;/h2&gt;

&lt;p&gt;Using the wrong parser for a document is either wasteful or produces empty output.&lt;/p&gt;

&lt;p&gt;PDFs are classified as simple or complex before parsing. Simple PDFs (clean text layer, no form fields) go to PyMuPDF4LLM: free, local, milliseconds. Complex PDFs (scanned pages, form fields, image-only content) route to Gemini 2.5 Flash via OpenRouter at roughly $0.0025 per page, with page-range pagination and adaptive batch halving when the model truncates its output. Oversized PDFs above a configurable page ceiling get a local peek of the first three pages for classification, then route to summary mode if the content is noise.&lt;/p&gt;

&lt;p&gt;Office documents are rendered locally. DOCX paragraphs become plain text, tables become GitHub Flavored Markdown with proper delimiters. XLSX sheets become headed sections with GFM tables. CSV files go through the charset fallback chain before table rendering. Only legacy binary formats (&lt;code&gt;.doc&lt;/code&gt;, &lt;code&gt;.xls&lt;/code&gt;, &lt;code&gt;.ppt&lt;/code&gt;) still require a cloud parser.&lt;/p&gt;

&lt;p&gt;One thing worth noting: when a local parser opens a file but extracts zero text (an image-only DOCX, for instance), a specific &lt;code&gt;EmptyContentError&lt;/code&gt; triggers fallback to the cloud parser. Generic errors from corrupt files do &lt;em&gt;not&lt;/em&gt; trigger the fallback, so you don’t waste API calls on unrecoverable files.&lt;/p&gt;

&lt;p&gt;Tiered routing cuts parsing costs by 28-42% compared to sending everything through cloud parsers, saving roughly $63-94 per full ingest of the production corpus.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;filtering-images-and-stripping-parser-artifacts&quot;&gt;Filtering images and stripping parser artifacts&lt;/h2&gt;

&lt;p&gt;Emails contain many non-informative images: logos, banners, tracking pixels, signature graphics. Each Vision API call costs roughly $0.01, and in a typical maritime email corpus, 60-80% of embedded images are logos and banners.&lt;/p&gt;

&lt;p&gt;A two-stage filter handles this. Heuristic rules hard-skip filenames matching patterns like &lt;code&gt;logo&lt;/code&gt;, &lt;code&gt;banner&lt;/code&gt;, &lt;code&gt;signature&lt;/code&gt;, or &lt;code&gt;icon&lt;/code&gt;. Files under 15KB (tracking pixels) or with dimensions under 100px are skipped. Images shorter than 50px with a width-to-height ratio above 3:1 are classified as email separator strips. Only images that pass these checks reach the Vision API, which classifies them as meaningful or decorative. The heuristic filter alone eliminates 30-50% of Vision API calls, saving $19-32 per full ingest.&lt;/p&gt;

&lt;p&gt;Parser artifacts are another source of noise. LLM-based parsers inject placeholder image references like &lt;code&gt;![](page_3_image_1.jpg)&lt;/code&gt; that point to non-existent files. Regex-based stripping removes these while preserving any alt text that carries semantic value. A maintenance command can retroactively apply improved stripping patterns to all archived markdown, so bumping the regex fixes older archives without re-parsing.&lt;/p&gt;

&lt;h2 id=&quot;deciding-what-to-embed&quot;&gt;Deciding what to embed&lt;/h2&gt;

&lt;p&gt;Sensor logs, repetitive tables, and empty PDFs waste embedding API calls and pollute the vector space. There is no reason to chunk and embed all of them the same way.&lt;/p&gt;

&lt;p&gt;An embedding mode classifier routes each attachment into one of three modes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code&gt;full&lt;/code&gt;: standard chunking with context and embeddings. Default for prose content.&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;summary&lt;/code&gt;: one LLM-synthesized summary chunk. Used for bulk numeric or tabular content (digit ratio above 30%, table content above 40%, high repetition scores).&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;metadata_only&lt;/code&gt;: one stub chunk from the filename, no LLM calls. For empty or near-empty attachments (under 50 tokens, or very low prose ratio with no headings).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The decision tree uses computed content shape signals: digit ratio, prose ratio, table character percentage, repetition score, short line ratio, heading count. For documents in a medium-confidence band, a single LLM triage call makes the final classification. The triage prompt includes an anti-injection preamble, and the response parser reads only the first character, so crafted attachments cannot steer their own classification.&lt;/p&gt;

&lt;p&gt;In the production corpus, 15-25% of attachments are sensor logs or repetitive tables. Summary mode reduces their embedding footprint by 95% while keeping them searchable.&lt;/p&gt;

&lt;h2 id=&quot;chunking-and-context-enrichment&quot;&gt;Chunking and context enrichment&lt;/h2&gt;

&lt;p&gt;Once content is clean and classified, it needs to be split into chunks that respect the embedding model’s token limits and the document’s logical structure.&lt;/p&gt;

&lt;p&gt;Token-aware splitting uses &lt;code&gt;tiktoken&lt;/code&gt; (matching the embedding model’s tokenizer) to keep chunks within the context window. Markdown content uses a &lt;code&gt;MarkdownTextSplitter&lt;/code&gt; that respects heading boundaries; plain text uses a recursive splitter with separators ordered from paragraph breaks down to character level. Splits under 5 words are discarded, since these are typically PDF pagination artifacts like lone page numbers.&lt;/p&gt;

&lt;p&gt;Each chunk records its character offset and line numbers in the original document, so the chat UI can highlight the exact source passage when citing a chunk. Heading hierarchy extraction tracks the structural path (e.g., &lt;code&gt;## Safety &amp;gt; ### Fire Prevention&lt;/code&gt;) for additional context.&lt;/p&gt;

&lt;p&gt;Contextual embedding had more effect on retrieval quality than anything else I tried. An LLM produces a 2-3 sentence summary of each document (type, subject, key entities), which is prepended to every chunk before embedding. A chunk like “The valve was replaced on Tuesday” becomes retrievable for queries about “MV Atlantic valve replacement” because the prepended context supplies the vessel name and subject. Research on contextual retrieval shows this improves retrieval accuracy by 35-67% over naive chunking.&lt;/p&gt;

&lt;p&gt;For email threads with multiple messages, a separate thread digest captures the full conversation in a 200-400 word summary chunk. When a user asks “how was the pump failure fixed?”, the digest matches because it connects the resolution in message 5 to the original problem in message 1.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;metadata-extraction-and-inheritance&quot;&gt;Metadata extraction and inheritance&lt;/h2&gt;

&lt;p&gt;Chunks from attachments have no context on their own. A PDF inspection report does not know it was attached to an email about “Main Engine Cylinder 3 Crack on MV ATLANTIC.” Without the parent email’s context, attachment chunks produce generic embeddings that miss filtered searches.&lt;/p&gt;

&lt;p&gt;Vessel mention extraction uses regex patterns for maritime naming conventions (&lt;code&gt;M/V [NAME]&lt;/code&gt;, &lt;code&gt;MV [NAME]&lt;/code&gt;) with a curated noise filter for non-vessel tokens (TANKERS, SHIP, CORP). Extracted vessel IDs go on every chunk, which makes per-vessel filtering possible during search.&lt;/p&gt;

&lt;p&gt;Attachment chunks inherit their parent email’s vessel IDs and topic IDs. A cargo damage report PDF shows up in “Cargo Damage” topic-filtered searches even if the PDF itself never mentions the topic by name. Both the direct-attachment and ZIP-member code paths use the same shared helper to prevent drift.&lt;/p&gt;

&lt;p&gt;Topic extraction uses an LLM to assign 1-5 topics per email, with aggressive name normalization and semantic deduplication via cosine similarity (threshold 0.92) to prevent the same concept from fragmenting across entries like “Cargo Damage”, “cargo damage”, and “Damaged Cargo.”&lt;/p&gt;

&lt;h2 id=&quot;storage-integrity-and-deduplication&quot;&gt;Storage integrity and deduplication&lt;/h2&gt;

&lt;p&gt;Re-ingesting emails after a parser upgrade, bug fix, or crash recovery must not create duplicate documents or chunks.&lt;/p&gt;

&lt;p&gt;Content-addressable IDs make this deterministic. Document IDs are &lt;code&gt;SHA256(source_id + file_hash)&lt;/code&gt;, chunk IDs derive from the document ID plus character offsets. Same content always produces the same IDs. The database enforces &lt;code&gt;UNIQUE(chunk_id)&lt;/code&gt; with &lt;code&gt;INSERT OR REPLACE&lt;/code&gt;, making chunk insertion idempotent. Foreign key cascading deletes prevent orphaned chunks when a document is re-processed. The progress tracker uses file content hash (not file path) to decide what needs processing, so renaming a file does not trigger re-ingest.&lt;/p&gt;

&lt;h2 id=&quot;validation-and-targeted-repair&quot;&gt;Validation and targeted repair&lt;/h2&gt;

&lt;p&gt;Bugs will slip through regardless. The last line of defense is a validation suite with 31 automated checks covering referential integrity (orphaned chunks, broken parent references), embedding completeness (missing vectors, NaN values, zero vectors), content quality (empty chunks, missing context summaries), archive consistency (broken URIs, orphaned folders), schema drift, mode invariants, and topic health.&lt;/p&gt;

&lt;p&gt;Every step stamps a trail in the archive metadata: which parser, which LLM model, which embedding model produced each artifact. When a check fails, the trail tells you exactly what to fix. “This PDF was parsed by Gemini 2.5 Flash with a 25-page batch” narrows the scope instantly.&lt;/p&gt;

&lt;p&gt;The system supports targeted repair over full re-ingest. A regex improvement needs only &lt;code&gt;clean-archive-md&lt;/code&gt; (zero API cost). A parser bug on specific emails uses &lt;code&gt;mark-failed&lt;/code&gt; plus &lt;code&gt;retry-failed&lt;/code&gt; (targeted cost). A chunking strategy change uses &lt;code&gt;reindex-chunks&lt;/code&gt; (moderate cost, no re-parsing). Full re-ingest is the last resort, since it costs real money and hours of wall-clock time at production scale.&lt;/p&gt;

&lt;h2 id=&quot;what-held-up&quot;&gt;What held up&lt;/h2&gt;

&lt;p&gt;Looking back, the techniques that moved retrieval accuracy the most were mundane: stripping boilerplate, fixing encodings, filtering junk images, making sure every chunk carries enough context to be found.&lt;/p&gt;

&lt;p&gt;Contextual embedding (prepending a document-level summary to every chunk) improved retrieval accuracy by 35-67%. Tiered parsing and embedding mode classification saved 28-42% on parsing and 95% on embedding costs for non-prose content. Content-addressable IDs and idempotent inserts meant I could iterate on the pipeline without worrying about corrupting production data or burning through API budgets. Automated validation after every ingest caught the data-dependent edge cases that unit tests never will.&lt;/p&gt;

&lt;p&gt;None of this is exciting work. But in a RAG system, it is the work that decides whether your users get the right answer.&lt;/p&gt;

&lt;h2 id=&quot;things-to-think-about&quot;&gt;Things to think about&lt;/h2&gt;

&lt;p&gt;A few concerns that fall outside the core data cleaning flow but are worth keeping in mind:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Error message sanitization.&lt;/strong&gt; API errors can contain Bearer tokens, API keys, or connection strings in their stack traces. If you store error messages (for debugging or admin UIs), strip credentials before persisting them.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ZIP security.&lt;/strong&gt; Email attachments can include ZIP archives with path traversal attacks (&lt;code&gt;../&lt;/code&gt; in filenames), ZIP bombs (high compression ratios that exhaust memory), or deeply nested archives. Configurable limits on extraction depth, file count, and total size prevent a single malicious attachment from stalling the pipeline. Office formats (&lt;code&gt;.docx&lt;/code&gt;, &lt;code&gt;.xlsx&lt;/code&gt;, &lt;code&gt;.pptx&lt;/code&gt;) are technically ZIP files and need to be detected and routed to their proper parsers instead of being extracted.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 16 Jun 2026 08:00:00 +0000</pubDate>
        <link>https://jwillmer.de/blog/tutorial/data-cleaning-for-rag-ingest-pipeline</link>
        <guid isPermaLink="true">https://jwillmer.de/blog/tutorial/data-cleaning-for-rag-ingest-pipeline</guid>
        
        <category>rag</category>
        
        <category>data-cleaning</category>
        
        <category>nlp</category>
        
        <category>embeddings</category>
        
        <category>llm</category>
        
        <category>ingest</category>
        
        <category>pipeline</category>
        
        <category>email-processing</category>
        
        
        <category>Tutorial</category>
        
      </item>
    
      <item>
        <title>Gear Ratio Advanced: Velocity, Torque &amp; Efficiency</title>
        <description>&lt;p&gt;This post builds on &lt;a href=&quot;https://jwillmer.de/blog/tutorial/gear-ratio-basics&quot;&gt;Gear Ratio Basics&lt;/a&gt; and explains how gear ratios affect speed (velocity), torque, and efficiency. Understanding these concepts helps you design gear systems that work the way you need them to.&lt;/p&gt;

&lt;h3 id=&quot;-gear-ratio-and-velocity&quot;&gt;🔄 Gear Ratio and Velocity&lt;/h3&gt;

&lt;p&gt;The gear ratio controls how fast the output gear turns compared to the input gear. The formula for calculating this is shown below:&lt;/p&gt;

\[\text{Gear Ratio} = \frac{\text{Input Speed}}{\text{Output Speed}}\]

&lt;p&gt;For example, if your input gear rotates at 200 revolutions per minute (RPM) and you use a 2:1 gear ratio, you can calculate the output speed like this:&lt;/p&gt;

\[\text{Output Speed} = \frac{200\, \text{RPM}}{2} = 100\, \text{RPM}\]

&lt;p&gt;This means the output gear turns at 100 RPM. A higher gear ratio slows down the output gear because the driven gear rotates fewer times for each turn of the driver gear. That’s how gear reductions let you decrease speed when needed, like in a robot’s drivetrain or a power tool’s gearbox.&lt;/p&gt;

&lt;h3 id=&quot;-gear-ratio-and-torque&quot;&gt;💪 Gear Ratio and Torque&lt;/h3&gt;

&lt;p&gt;Gear ratios also affect torque, which is the twisting force transmitted through the gears. The relationship between gear ratio and torque is:&lt;/p&gt;

\[\text{Output Torque} = \text{Input Torque} \times \text{Gear Ratio}\]

&lt;p&gt;For example, if your motor produces 1 newton-meter (Nm) of torque and you use a 3:1 gear ratio, you calculate the output torque like this:&lt;/p&gt;

\[\text{Output Torque} = 1\, \text{Nm} \times 3 = 3\, \text{Nm}\]

&lt;p&gt;This means you get 3 Nm of torque at the output. By slowing the rotation, the gear system lets the same power apply more force. In practical terms, a gear reduction gives you more torque at the cost of slower speed, which is essential when you need to move something heavy.&lt;/p&gt;

&lt;h3 id=&quot;️-direction-reminder&quot;&gt;⚠️ Direction Reminder&lt;/h3&gt;

&lt;p&gt;Whenever gears mesh, they change the direction of rotation. Every time you add a gear between the driver and the driven gear, the direction reverses again. For example, if you connect your driver gear to an idler gear, the idler reverses the direction, and the next gear will spin the opposite way compared to the driver. An idler gear does not change the gear ratio, but it flips the rotation direction once more. An even number of gears in the train means the output spins in the same direction as the input. An odd number of gears means the output spins in the opposite direction.&lt;/p&gt;

&lt;h3 id=&quot;️-gear-efficiency&quot;&gt;⚙️ Gear Efficiency&lt;/h3&gt;

&lt;p&gt;No gear system is perfectly efficient. Energy is lost through friction, heat, and flexing of materials, so it is important to understand gear efficiency. Spur gears usually operate at 95 to 98 percent efficiency for each gear mesh. Worm gears have much lower efficiency, often below 70 percent, because of the sliding friction that happens between the worm and worm wheel.&lt;/p&gt;

&lt;p&gt;To calculate the total efficiency of a gear train with multiple stages, multiply the efficiency of each gear pair together. For example, if you have two spur gear pairs with 97 percent efficiency each, you calculate the total efficiency like this:&lt;/p&gt;

\[\text{Total Efficiency} = 0.97 \times 0.97 = 0.9409 \quad (\text{or } 94.1\%)\]

&lt;p&gt;This means your gear train will deliver about 94.1 percent of the input power to the output. Lower efficiency means more heat and less useful power, which can become a problem in systems where energy savings or heat management are important.&lt;/p&gt;

&lt;h3 id=&quot;-summary&quot;&gt;✅ Summary&lt;/h3&gt;

&lt;p&gt;A higher gear ratio reduces the output speed but increases the output torque. Gears reverse rotation direction with each mesh, and the total number of gears in a train determines whether the output spins in the same or opposite direction as the input. Efficiency losses add up with every gear pair, so always include them in your calculations when designing a gear system.&lt;/p&gt;
</description>
        <pubDate>Fri, 27 Jun 2025 21:21:00 +0000</pubDate>
        <link>https://jwillmer.de/blog/tutorial/gear-ratio-advanced</link>
        <guid isPermaLink="true">https://jwillmer.de/blog/tutorial/gear-ratio-advanced</guid>
        
        <category>gear</category>
        
        <category>ratio</category>
        
        <category>calculation</category>
        
        <category>mechanics</category>
        
        
        <category>Tutorial</category>
        
      </item>
    
      <item>
        <title>What is Retrieval-Augmented Generation (RAG)</title>
        <description>&lt;p&gt;Large language models are impressive, but they’re limited by what they were trained on. They can’t access your internal documentation, stay current with new data, or reliably distinguish fact from fiction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieval-Augmented Generation (RAG)&lt;/strong&gt; addresses this gap. It augments a language model by giving it access to external data at runtime. When a question is asked, the system first retrieves relevant information from a knowledge base—usually a vector database of semantically indexed chunks. Only then does the model generate a response, grounded in this retrieved context.&lt;/p&gt;

&lt;p&gt;This enables more accurate, domain-aware, and verifiable answers without retraining the model. RAG effectively gives language models a dynamic memory—on your terms.&lt;/p&gt;

&lt;h2 id=&quot;when-to-use-rag&quot;&gt;&lt;strong&gt;When to Use RAG&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;Retrieval-Augmented Generation is ideal when your application needs accurate, current, and domain-specific responses—but you don’t want to (or can’t) retrain the model.&lt;/p&gt;

&lt;p&gt;Use RAG when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Your data changes frequently.&lt;/strong&gt; Traditional fine-tuning locks knowledge at training time. RAG lets you update answers by simply changing the underlying documents.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;You need traceability.&lt;/strong&gt; With RAG, every response is backed by retrievable content. Users (or auditors) can trace outputs to their original source.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Your knowledge is proprietary.&lt;/strong&gt; Whether it’s internal policies, customer reports, or technical documentation, RAG can surface private data securely at inference time.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;You want modular updates.&lt;/strong&gt; By storing and referencing chunks with unique IDs, you can update individual pieces of content without retraining or reindexing everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RAG is especially useful for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Internal support agents&lt;/li&gt;
  &lt;li&gt;Developer or product documentation assistants&lt;/li&gt;
  &lt;li&gt;Compliance and legal tools&lt;/li&gt;
  &lt;li&gt;Systems needing multilingual or version-aware responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app needs &lt;em&gt;dynamic answers with real references&lt;/em&gt;, RAG is the right foundation. It’s not just a theoretical model—real systems are using it to solve hard problems today.&lt;/p&gt;

&lt;p&gt;Real-world systems already use this architecture to great effect. For instance, &lt;a href=&quot;https://mem0.com/&quot;&gt;Mem0&lt;/a&gt; implements a memory layer built on RAG. It retrieves semantically indexed memory entries—rather than relying on fragile prompt chains—enabling consistent, personalized responses over time.&lt;/p&gt;

&lt;p&gt;At the infrastructure level, vector search engines like &lt;a href=&quot;https://qdrant.tech/&quot;&gt;Qdrant&lt;/a&gt; power these retrieval systems. Qdrant supports hybrid filtering, payload scoring, and fast nearest neighbor search, making it ideal for large-scale, production-grade RAG systems.&lt;/p&gt;

&lt;h2 id=&quot;preparing-data-for-ingestion&quot;&gt;&lt;strong&gt;Preparing Data for Ingestion&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;To make RAG work reliably, your content must be structured for retrieval and usable by a language model. This isn’t about dumping documents into a vector database—it’s about shaping the content so the model can reason over it effectively.&lt;/p&gt;

&lt;p&gt;Start by &lt;strong&gt;extracting clean content&lt;/strong&gt; from your sources. Remove layout artifacts, navigation elements, and anything irrelevant to the actual information. You want concise, plain-language text that reflects what a human would read to understand the topic.&lt;/p&gt;

&lt;p&gt;Next, &lt;strong&gt;normalize the language&lt;/strong&gt;. Rewrite content — especially code snippets, config files, or logs — into complete, natural sentences. Instead of embedding a raw string like &lt;code&gt;user_limit: 250&lt;/code&gt;, convert it to “The maximum number of users allowed is 250.” The goal is natural language that the model can easily process and use in a response.&lt;/p&gt;

&lt;p&gt;Every chunk should include &lt;strong&gt;descriptive metadata&lt;/strong&gt;. This includes values like the source URL, section title, date, author, product or version, and any tags or classifications you use internally. Metadata can be stored separately or embedded into the text depending on your system design, but it must be consistent and queryable.&lt;/p&gt;

&lt;p&gt;Finally, and critically, assign a &lt;strong&gt;unique and stable ID&lt;/strong&gt; to every chunk or document. This lets you update or delete specific entries later without affecting the rest of your dataset. It’s essential for keeping your index maintainable over time.&lt;/p&gt;

&lt;p&gt;RAG is only as good as the data it retrieves—so preparing your content with care is the foundation for everything that follows.&lt;/p&gt;

&lt;h2 id=&quot;improving-retrieval-with-context&quot;&gt;&lt;strong&gt;Improving Retrieval with Context&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;Once your data is clean, natural, and enriched with metadata, the next step is making it &lt;strong&gt;retrievable in a meaningful way&lt;/strong&gt;. This is where context comes in. A single paragraph or sentence often lacks enough information on its own to match a user’s query effectively. By embedding &lt;strong&gt;context into each chunk&lt;/strong&gt;, you improve both recall and precision during retrieval.&lt;/p&gt;

&lt;p&gt;Inspired by &lt;a href=&quot;https://www.anthropic.com/news/contextual-retrieval&quot;&gt;Anthropic’s contextual retrieval approach&lt;/a&gt;, one method is to &lt;strong&gt;prepend a short description&lt;/strong&gt; that explains what the chunk is about. For example, instead of storing:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“The system will reject login attempts after 5 failed tries.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You might store:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“From the user authentication section of the security policy: The system will reject login attempts after 5 failed tries.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This extra framing helps the embedding model encode &lt;em&gt;why&lt;/em&gt; this text matters and &lt;em&gt;where&lt;/em&gt; it belongs. It also improves match quality for more abstract or high-level questions like “What are our login security rules?”&lt;/p&gt;

&lt;p&gt;Context can come from:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Section headings or document structure&lt;/li&gt;
  &lt;li&gt;File paths or category tags&lt;/li&gt;
  &lt;li&gt;Summaries or topic labels&lt;/li&gt;
  &lt;li&gt;Manual annotations (if scale allows)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addition to prepending context to text, you can enrich your vector index with structured metadata. Many systems support hybrid search—combining vector similarity with keyword filters. For example, you can restrict results by audience (&lt;code&gt;developer&lt;/code&gt;), language (&lt;code&gt;de&lt;/code&gt;), or document type (&lt;code&gt;release_notes&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The key idea is: &lt;strong&gt;make the meaning explicit&lt;/strong&gt;. Give the system as much information as possible, up front, to help it retrieve the right content later.&lt;/p&gt;

&lt;h2 id=&quot;conclusion-why-rag-matters&quot;&gt;&lt;strong&gt;Conclusion: Why RAG Matters&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;RAG brings structure, memory, and accountability to generative systems. It bridges the gap between static models and real-world knowledge — without the overhead of retraining.&lt;/p&gt;

&lt;p&gt;To recap:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Use RAG when your data changes often, needs to stay private, or must be cited.&lt;/li&gt;
  &lt;li&gt;Prepare your data with clean, readable language and real metadata.&lt;/li&gt;
  &lt;li&gt;Track unique IDs for each entry so your dataset stays maintainable.&lt;/li&gt;
  &lt;li&gt;Add contextual information to each chunk to improve retrieval precision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Done right, RAG systems are more flexible than fine-tuning, more trustworthy than standalone LLMs, and more adaptable to your evolving needs.&lt;/p&gt;

&lt;p&gt;If you’re building AI systems that need to be smart &lt;em&gt;and&lt;/em&gt; reliable, RAG isn’t just an option—it’s the standard.&lt;/p&gt;
</description>
        <pubDate>Tue, 20 May 2025 06:20:00 +0000</pubDate>
        <link>https://jwillmer.de/blog/tutorial/retrieval-augmented-generation</link>
        <guid isPermaLink="true">https://jwillmer.de/blog/tutorial/retrieval-augmented-generation</guid>
        
        <category>rag</category>
        
        <category>llm</category>
        
        <category>agent</category>
        
        <category>ai</category>
        
        <category>vector-search</category>
        
        <category>memory-layer</category>
        
        <category>semantic-search</category>
        
        <category>retrieval-augmented-generation</category>
        
        
        <category>Tutorial</category>
        
      </item>
    
      <item>
        <title>Docker Networking Pitfalls</title>
        <description>&lt;p&gt;Docker networking is powerful but can be confusing, especially when dealing with communication between the host machine and Docker containers. Many developers assume that network behavior inside a Docker network works the same as on the host, leading to unexpected issues. In this post, we’ll explore common pitfalls when running services with Docker Compose and how to handle them correctly.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-basics-host-vs-docker-network&quot;&gt;The Basics: Host vs. Docker Network&lt;/h2&gt;
&lt;p&gt;Docker Compose creates an isolated network for services to communicate. Each container gets a unique DNS name corresponding to its service name in &lt;code&gt;docker-compose.yml&lt;/code&gt;. However, the way networking works inside Docker is different from how it works on the host machine.&lt;/p&gt;

&lt;h3 id=&quot;host-perspective-external-access&quot;&gt;&lt;strong&gt;Host Perspective (External Access)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;When accessing a service from the host machine (outside Docker), you must use &lt;code&gt;localhost&lt;/code&gt; and the mapped port:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://localhost:8080  # Accessing a container-bound service via a mapped port
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You &lt;strong&gt;cannot&lt;/strong&gt; use Docker-internal DNS names (such as &lt;code&gt;service_name&lt;/code&gt;) or &lt;code&gt;host.docker.internal&lt;/code&gt; from the host machine.&lt;/p&gt;

&lt;h3 id=&quot;docker-container-perspective-internal-communication&quot;&gt;&lt;strong&gt;Docker Container Perspective (Internal Communication)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Containers within the same Docker network can refer to each other by service name:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://app-store:5000  # Works inside Docker
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But if a container needs to communicate with a service running &lt;strong&gt;on the host machine&lt;/strong&gt;, it must use &lt;code&gt;host.docker.internal&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://host.docker.internal:3000  # Works inside Docker to reach host
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;common-pitfalls-and-how-to-solve-them&quot;&gt;Common Pitfalls and How to Solve Them&lt;/h2&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;pitfall-1-trying-to-access-a-container-using-service-names-from-the-host&quot;&gt;&lt;strong&gt;Pitfall 1: Trying to Access a Container Using Service Names from the Host&lt;/strong&gt;&lt;/h3&gt;
&lt;h4 id=&quot;-incorrect&quot;&gt;❌ Incorrect:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://app-store:5000  # Won&apos;t work from the host machine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;app-store&lt;/code&gt; service name is only resolvable inside Docker’s internal network.&lt;/p&gt;

&lt;h4 id=&quot;-correct&quot;&gt;✅ Correct:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://localhost:5000  # Use localhost + mapped port from the host
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works if the port is correctly mapped in &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;docker-compose.yml:
services:
  app-store:
    ports:
      - &quot;5000:5000&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;pitfall-2-using-localhost-inside-a-container-to-reach-another-container&quot;&gt;&lt;strong&gt;Pitfall 2: Using &lt;code&gt;localhost&lt;/code&gt; Inside a Container to Reach Another Container&lt;/strong&gt;&lt;/h3&gt;
&lt;h4 id=&quot;-incorrect-1&quot;&gt;❌ Incorrect:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://localhost:5000  # Won&apos;t work inside a container
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside a container, &lt;code&gt;localhost&lt;/code&gt; refers to &lt;strong&gt;itself&lt;/strong&gt;, not other services in the Docker network.&lt;/p&gt;

&lt;h4 id=&quot;-correct-1&quot;&gt;✅ Correct:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://app-store:5000  # Use the service name inside Docker
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;pitfall-3-a-container-trying-to-access-a-host-service-without-hostdockerinternal&quot;&gt;&lt;strong&gt;Pitfall 3: A Container Trying to Access a Host Service Without &lt;code&gt;host.docker.internal&lt;/code&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;h4 id=&quot;-incorrect-2&quot;&gt;❌ Incorrect:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://localhost:3306  # Won&apos;t work inside Docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since &lt;code&gt;localhost&lt;/code&gt; inside a container refers to the container itself, this won’t connect to a host service.&lt;/p&gt;

&lt;h4 id=&quot;-correct-2&quot;&gt;✅ Correct:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://host.docker.internal:3306  # Correct way inside Docker
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;pitfall-4-web-applications-generating-incorrect-urls&quot;&gt;&lt;strong&gt;Pitfall 4: Web Applications Generating Incorrect URLs&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Web applications often generate links for users dynamically based on their environment. If the application is running inside a Docker container, it may generate links using internal Docker service names, which are not accessible to users.&lt;/p&gt;

&lt;h4 id=&quot;-incorrect-3&quot;&gt;❌ Incorrect:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;a href=&quot;http://web-service:8080&quot;&amp;gt;Click here&amp;lt;/a&amp;gt;  &amp;lt;!-- Won&apos;t work for the user --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&quot;-correct-3&quot;&gt;✅ Correct:&lt;/h4&gt;
&lt;p&gt;Ensure the application differentiates between:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Internal URLs&lt;/strong&gt; (used by services within Docker, such as &lt;code&gt;web-service:8080&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;External URLs&lt;/strong&gt; (used by users, such as &lt;code&gt;localhost:8080&lt;/code&gt; or a domain name)&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;understanding-hostdockerinternal-availability&quot;&gt;Understanding &lt;code&gt;host.docker.internal&lt;/code&gt; Availability&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;host.docker.internal&lt;/code&gt; is a special hostname that resolves to the host machine’s IP address from within a Docker container. However, its availability depends on the operating system:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Platform&lt;/th&gt;
      &lt;th&gt;Availability&lt;/th&gt;
      &lt;th&gt;Notes&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Windows&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;✅ Available&lt;/td&gt;
      &lt;td&gt;Works out of the box&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Mac (Intel &amp;amp; M1/M2)&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;✅ Available&lt;/td&gt;
      &lt;td&gt;Built-in since Docker Desktop 18.03&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Linux&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;❌ Not Available&lt;/td&gt;
      &lt;td&gt;Requires manual setup via &lt;code&gt;extra_hosts&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;WSL 2&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;✅ Available&lt;/td&gt;
      &lt;td&gt;Works with Docker Desktop&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;iOS&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;❌ Not Available&lt;/td&gt;
      &lt;td&gt;No official support&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;For Linux users, a workaround is required:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;docker-compose.yml:
services:
  my-service:
    extra_hosts:
      - &quot;host.docker.internal:host-gateway&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This maps &lt;code&gt;host.docker.internal&lt;/code&gt; to the host gateway.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;visualizing-the-networking-model&quot;&gt;Visualizing the Networking Model&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Source&lt;/th&gt;
      &lt;th&gt;Destination&lt;/th&gt;
      &lt;th&gt;Works?&lt;/th&gt;
      &lt;th&gt;Solution&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Host → Container&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;localhost:port&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt;Use mapped port in &lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Host → Container&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;service_name&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;Won’t resolve&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Container → Container&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;service_name&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt;Works within the same Docker network&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Container → Host&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;localhost&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;Refers to itself&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Container → Host&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;host.docker.internal:port&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt;Works correctly&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Web App → User Link&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;service_name:port&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;❌&lt;/td&gt;
      &lt;td&gt;Won’t work for users&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Web App → User Link&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;localhost:port&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;✅&lt;/td&gt;
      &lt;td&gt;Use proper externally accessible URLs&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h2&gt;

&lt;figure&gt;
   &lt;img src=&quot;https://jwillmer.de/media/img/2025-02-02-docker-networking-pitfalls.drawio.png&quot; /&gt;
   &lt;figcaption&gt;Overview of the communication flow&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Use &lt;code&gt;localhost:port&lt;/code&gt; to access Docker services from the host.&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Use service names for inter-container communication inside Docker.&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Use &lt;code&gt;host.docker.internal&lt;/code&gt; for Docker-to-host communication (if supported).&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Linux requires manual configuration for &lt;code&gt;host.docker.internal&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Mapped ports are crucial for external access.&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ensure web applications generate URLs users can actually reach.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By keeping these rules in mind, you can avoid common networking pitfalls when using Docker Compose. Share this guide with your colleagues to clarify how Docker networking works and improve debugging efficiency!&lt;/p&gt;
</description>
        <pubDate>Sun, 02 Feb 2025 21:15:00 +0000</pubDate>
        <link>https://jwillmer.de/blog/programming/docker-networking-pitfalls</link>
        <guid isPermaLink="true">https://jwillmer.de/blog/programming/docker-networking-pitfalls</guid>
        
        <category>docker</category>
        
        <category>docker-compose</category>
        
        <category>networking</category>
        
        <category>devops</category>
        
        <category>containers</category>
        
        <category>port-mapping</category>
        
        
        <category>Programming</category>
        
      </item>
    
      <item>
        <title>Interactive Motion Extraction with JavaScript</title>
        <description>&lt;h1 id=&quot;motion-extraction-in-action-real-time-video-processing-with-javascript&quot;&gt;Motion Extraction in Action: Real-time Video Processing with JavaScript&lt;/h1&gt;

&lt;p&gt;In this blog post, we’ll explore a motion extraction technique inspired by an approach presented in &lt;a href=&quot;https://www.youtube.com/watch?v=NSS6yAMZF78&quot;&gt;this YouTube video by Steve of CodeParade&lt;/a&gt;. Steve’s work showcases impressive concepts in image processing, and this implementation applies similar ideas using JavaScript to manipulate video frames in real time. You can interact with the code embedded below or visit the GitHub gist for the complete snippet.&lt;/p&gt;

&lt;h2 id=&quot;how-motion-extraction-works&quot;&gt;How Motion Extraction Works&lt;/h2&gt;

&lt;p&gt;Motion extraction refers to isolating differences between frames in a video or stream. This technique can be useful in scenarios like surveillance, where detecting motion helps trigger specific events, or for analyzing movements in sports or other activities.&lt;/p&gt;

&lt;p&gt;This example utilizes the following core steps:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Frame Capture&lt;/strong&gt;: We take snapshots of a video feed (from the camera or an uploaded file).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Color Inversion&lt;/strong&gt;: Each frame is inverted to enhance the contrast between motion and background.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Motion Detection&lt;/strong&gt;: By comparing consecutive frames, we cancel out parts that remain static, highlighting only the regions that exhibit change (motion).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Display&lt;/strong&gt;: The processed frame is displayed, and the effect is repeated in intervals to create continuous motion detection.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Users can adjust parameters like &lt;strong&gt;resolution&lt;/strong&gt; and &lt;strong&gt;delay&lt;/strong&gt; to experiment with the results.&lt;/p&gt;

&lt;h2 id=&quot;key-features-of-the-code&quot;&gt;Key Features of the Code&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Live or Uploaded Video&lt;/strong&gt;: You can toggle between your camera feed and an uploaded video file.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Freeze Frame&lt;/strong&gt;: This feature freezes one frame so that you will see all motion changes in respect to the frozen frame.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Inverted Color Blending&lt;/strong&gt;: The code uses color inversion and blending between consecutive frames to cancel out static areas, highlighting only moving objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;freeze-frame-example&quot;&gt;Freeze Frame Example&lt;/h3&gt;

&lt;p&gt;Imagine you activate the freeze-frame option, and at that moment, a grey background is captured, like in the image below:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/media/img/2024-10-13-motion-extraction_no-motion.jpg&quot; alt=&quot;Frozen Grey Scene&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now, when you remove an object from the scene—such as a highlighting pen—the motion extraction process will clearly detect what has changed. In the following image, you can see the pen, which was removed, now highlighted by the motion extraction algorithm:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/media/img/2024-10-13-motion-extraction_missing-item.jpg&quot; alt=&quot;Motion Detected: Removed Pen&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This technique is highly effective in identifying what has been removed or changed in the scene by comparing the captured freeze-frame with subsequent frames.&lt;/p&gt;

&lt;h3 id=&quot;try-it-out&quot;&gt;Try it Out&lt;/h3&gt;
&lt;p&gt;Here is the live version of the code, which you can interact with to test motion extraction:&lt;/p&gt;

&lt;iframe src=&quot;/assets/posts/motion-extraction.html&quot; width=&quot;100%&quot; height=&quot;800px&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;For those who want to dive deeper into the code, you can check out the &lt;a href=&quot;https://gist.github.com/jwillmer/06d5d71f794cac86a45f40c18ae21fc1&quot;&gt;GitHub Gist&lt;/a&gt;.&lt;/p&gt;
</description>
        <pubDate>Sun, 13 Oct 2024 19:15:00 +0000</pubDate>
        <link>https://jwillmer.de/blog/programming/motion-extraction</link>
        <guid isPermaLink="true">https://jwillmer.de/blog/programming/motion-extraction</guid>
        
        <category>motion</category>
        
        <category>extraction</category>
        
        <category>detection</category>
        
        <category>javascript</category>
        
        <category>video</category>
        
        <category>processing</category>
        
        <category>surveillance</category>
        
        
        <category>Programming</category>
        
      </item>
    
      <item>
        <title>Personal Docker Cheat Sheet</title>
        <description>&lt;p&gt;This is a personal reference for Docker commands that are not used often but frequently need to be looked up. Instead of searching every time, this list provides direct access to those less common yet essential commands.&lt;/p&gt;

&lt;h4 id=&quot;remove-all-stopped-containers&quot;&gt;&lt;strong&gt;Remove All Stopped Containers&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rm $(docker ps -a -q)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Explanation&lt;/strong&gt;: This command removes all stopped containers.&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;docker ps -a -q&lt;/code&gt;: Lists the IDs of all containers (stopped and running).&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;docker rm&lt;/code&gt;: Removes the listed containers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;remove-all-docker-images-not-in-use&quot;&gt;&lt;strong&gt;Remove All Docker Images Not In Use&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rmi $(docker images -q)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Explanation&lt;/strong&gt;: This command removes all Docker images.&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;docker images -q&lt;/code&gt;: Lists the IDs of all images.&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;docker rmi&lt;/code&gt;: Removes the listed images.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;check-docker-disk-usage&quot;&gt;&lt;strong&gt;Check Docker Disk Usage&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker system df -v
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Explanation&lt;/strong&gt;: Shows detailed information about Docker disk usage, including volumes, images, containers, and how much space they occupy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;remove-all-unused-volumes-except-specified-ones&quot;&gt;&lt;strong&gt;Remove All Unused Volumes Except Specified Ones&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Test Command (preview volumes to be deleted):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker volume ls -qf dangling=true | grep -vE &apos;volume1|volume2&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Actual Removal Command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker volume ls -qf dangling=true | grep -vE &apos;volume1|volume2&apos; | xargs -r docker volume rm
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Explanation&lt;/strong&gt;: This command removes all unused (dangling) Docker volumes except the specified ones.&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;docker volume ls -qf dangling=true&lt;/code&gt;: Lists all unused volumes.
    &lt;ul&gt;
      &lt;li&gt;&lt;code&gt;-q&lt;/code&gt;: Shows only the volume names.&lt;/li&gt;
      &lt;li&gt;&lt;code&gt;-f dangling=true&lt;/code&gt;: Filters to list only the volumes that are dangling (unused).&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code&gt;grep -vE &apos;volume1|volume2&apos;&lt;/code&gt;: Filters out the volumes you want to keep (replace &lt;code&gt;volume1&lt;/code&gt;, &lt;code&gt;volume2&lt;/code&gt;, etc.).&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;xargs -r docker volume rm&lt;/code&gt;: Passes the remaining volume names to &lt;code&gt;docker volume rm&lt;/code&gt; and removes them. The &lt;code&gt;-r&lt;/code&gt; option ensures the command is only run if there are volumes to delete.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 19 Sep 2024 13:35:00 +0000</pubDate>
        <link>https://jwillmer.de/blog/tutorial/docker-cheat-sheet</link>
        <guid isPermaLink="true">https://jwillmer.de/blog/tutorial/docker-cheat-sheet</guid>
        
        <category>docker</category>
        
        <category>commands</category>
        
        <category>cheat</category>
        
        <category>sheet</category>
        
        
        <category>Tutorial</category>
        
      </item>
    
      <item>
        <title>Zero Downtime Deployment with Docker Rollout</title>
        <description>&lt;p&gt;This guide demonstrates a &lt;strong&gt;Zero Downtime Deployment&lt;/strong&gt; using &lt;a href=&quot;https://github.com/Wowu/docker-rollout&quot;&gt;&lt;strong&gt;Docker Rollout&lt;/strong&gt;&lt;/a&gt;, featuring an intentional failure, rollback scenario, and a successful rollout. Additional I demonstrate the use of a &lt;a href=&quot;https://traefik.io/traefik/&quot;&gt;Traefik reverse proxy&lt;/a&gt; to expose the service ports.&lt;/p&gt;

&lt;h4 id=&quot;prerequisites&quot;&gt;Prerequisites:&lt;/h4&gt;
&lt;p&gt;Ensure you have:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt; and &lt;strong&gt;Docker Compose&lt;/strong&gt; installed.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/Wowu/docker-rollout#installation&quot;&gt;&lt;strong&gt;Docker Rollout&lt;/strong&gt; installed&lt;/a&gt;:&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Create directory for Docker cli plugins
mkdir -p ~/.docker/cli-plugins

# Download docker-rollout script to Docker cli plugins directory
curl https://raw.githubusercontent.com/wowu/docker-rollout/master/docker-rollout -o ~/.docker/cli-plugins/docker-rollout

# Make the script executable
chmod +x ~/.docker/cli-plugins/docker-rollout
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&quot;directory-setup&quot;&gt;Directory Setup&lt;/h4&gt;

&lt;p&gt;Create the following files in your working directory:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dockerfile-1&lt;/strong&gt; (working version):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Dockerfile&quot;&gt;# Use the official NGINX base image
FROM nginx:latest

# Create a custom text file with &quot;Hello World V1&quot;
RUN echo &quot;Hello World V1&quot; &amp;gt; /usr/share/nginx/html/index.html

# Expose port 80 (the default NGINX port)
EXPOSE 80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Dockerfile-2&lt;/strong&gt; (intentional failure):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Dockerfile&quot;&gt;# Use the official NGINX base image
FROM nginx:latest

# Create a custom text file with &quot;Hello World V2&quot;
RUN echo &quot;Hello World V2&quot; &amp;gt; /usr/share/nginx/html/index.html

# Expose port 8080 (causing health check failure due to mismatch)
EXPOSE 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Version 2 exposes port 8080, which will cause the health check to fail initially as it is configured to check port 80.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;docker-compose.yml&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  web:
    image: static-site:1
    restart: always
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;curl -f http://localhost:80 || exit 1&quot;]
      interval: 10s
      timeout: 5s
      retries: 2
      start_period: 5s
    networks:
      - my_network

  test:
    image: alpine
    container_name: test
    command: sh -c &quot;apk add --no-cache wget &amp;amp;&amp;amp; tail -f /dev/null&quot;
    networks:
      - my_network

networks:
  my_network:
    driver: bridge
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&quot;build-docker-images&quot;&gt;Build Docker Images&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;Build version 1:&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker build -t static-site:1 -f Dockerfile-1 .
&lt;/code&gt;&lt;/pre&gt;

&lt;ol&gt;
  &lt;li&gt;Build version 2 (the failing one):&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker build -t static-site:2 -f Dockerfile-2 .
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&quot;run-the-initial-version&quot;&gt;Run the Initial Version&lt;/h4&gt;

&lt;p&gt;Start the services using Docker Compose:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This will start version 1 (&lt;code&gt;static-site:1&lt;/code&gt;) of the web service.&lt;/p&gt;

&lt;h4 id=&quot;test-the-initial-deployment&quot;&gt;Test the Initial Deployment&lt;/h4&gt;

&lt;p&gt;Verify that version 1 is running by executing &lt;code&gt;wget&lt;/code&gt; inside the &lt;code&gt;test&lt;/code&gt; container:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -it test wget -qO- http://web
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You should see:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Hello World V1
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&quot;update-the-web-service-version-for-rollout&quot;&gt;Update the Web Service Version for Rollout&lt;/h4&gt;

&lt;p&gt;Before running Docker Rollout, update the image of the &lt;code&gt;web&lt;/code&gt; service to &lt;strong&gt;version 2&lt;/strong&gt; in your &lt;code&gt;docker-compose.yml&lt;/code&gt; file:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;web:
  image: static-site:2
  # rest of the configuration remains the same
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Updating the version in the &lt;code&gt;docker-compose.yml&lt;/code&gt; file is necessary before executing the rollout command to initiate the update.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;perform-zero-downtime-deployment-with-docker-rollout&quot;&gt;Perform Zero Downtime Deployment with Docker Rollout&lt;/h4&gt;

&lt;p&gt;Run Docker Rollout to perform the zero downtime deployment:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rollout web --file docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;observations-during-rollout&quot;&gt;Observations During Rollout:&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;No downtime for the web service&lt;/strong&gt;: The web service remains fully operational during the update process.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;One container becomes unhealthy&lt;/strong&gt;: Since version 2 exposes a different port (&lt;code&gt;8080&lt;/code&gt;), the new version’s container will fail the health check, causing the deployment to fail.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Automatic rollback&lt;/strong&gt;: Docker Rollout will automatically roll back to version 1, ensuring the service remains healthy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;update-the-health-check-for-a-successful-rollout&quot;&gt;Update the Health Check for a Successful Rollout&lt;/h4&gt;

&lt;p&gt;To observe a successful rollout, update the health check in the &lt;code&gt;docker-compose.yml&lt;/code&gt; file to match port &lt;code&gt;8080&lt;/code&gt;, which version 2 is using:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;healthcheck:
  test: [&quot;CMD-SHELL&quot;, &quot;curl -f http://localhost:8080 || exit 1&quot;]
  interval: 10s
  timeout: 5s
  retries: 2
  start_period: 5s
&lt;/code&gt;&lt;/pre&gt;

&lt;ol&gt;
  &lt;li&gt;Update the &lt;code&gt;docker-compose.yml&lt;/code&gt; file with the health check for port &lt;code&gt;8080&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Run the rollout again with:&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rollout web --file docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;observations-during-successful-rollout&quot;&gt;Observations During Successful Rollout:&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;The service will now update successfully from version 1 to version 2, as the health check matches the correct port.&lt;/li&gt;
  &lt;li&gt;You should now see the new version output by running:&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -it test wget -qO- http://web
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Output:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Hello World V2
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&quot;setting-up-traefik-reverse-proxy-for-http-access&quot;&gt;Setting up Traefik Reverse Proxy for HTTP Access&lt;/h4&gt;

&lt;p&gt;You can use &lt;a href=&quot;https://traefik.io/traefik/&quot;&gt;&lt;strong&gt;Traefik&lt;/strong&gt;&lt;/a&gt; to expose your services externally without needing to define specific ports or container names in your Docker Compose file. This is especially useful when working with the Docker Rollout plugin, as it imposes &lt;strong&gt;limitations similar to Docker Swarm&lt;/strong&gt;, where ports or container names cannot be explicitly defined. Traefik simplifies this by handling service discovery and routing dynamically, allowing you to maintain external accessibility while supporting zero downtime deployments.&lt;/p&gt;

&lt;p&gt;By routing traffic based on service labels instead of container specifics, Traefik enables seamless updates and rollbacks without requiring manual changes to the container’s network settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;docker-compose.yml&lt;/strong&gt; with Traefik added:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &apos;3&apos;
services:
  web:
    image: static-site:1
    restart: always
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;curl -f http://localhost:80 || exit 1&quot;]
      interval: 10s
      timeout: 5s
      retries: 2
      start_period: 5s
    networks:
      - my_network
    labels:
      - &quot;traefik.enable=true&quot;
      - &quot;traefik.http.routers.web.rule=PathPrefix(`/`)&quot;
      - &quot;traefik.http.services.web.loadbalancer.server.port=80&quot;

  traefik:
    image: traefik:v2.4
    command:
      - &quot;--providers.docker=true&quot;
      - &quot;--entrypoints.web.address=:80&quot;
    ports:
      - &quot;80:80&quot;
    networks:
      - my_network

  test:
    image: alpine
    container_name: test
    command: sh -c &quot;apk add --no-cache wget &amp;amp;&amp;amp; tail -f /dev/null&quot;
    networks:
      - my_network

networks:
  my_network:
    driver: bridge
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;

&lt;p&gt;The Docker Rollout plugin enables near zero downtime deployments by updating service instances one at a time, ensuring that previous versions remain active until the new ones pass health checks. While the plugin strives to eliminate downtime, there is still a small possibility of downtime during the transition, as noted in &lt;a href=&quot;https://github.com/Wowu/docker-rollout/issues/21&quot;&gt;Issue #21&lt;/a&gt;, which is currently being worked on and may be resolved in the future.&lt;/p&gt;

&lt;p&gt;Additionally, using the restart: always policy is discouraged in favor of restart: unless-stopped, as outlined in &lt;a href=&quot;https://github.com/Wowu/docker-rollout/issues/25&quot;&gt;Issue #25&lt;/a&gt;. This prevents conflicts with Docker Rollout’s management of container lifecycles during updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features of Docker Rollout:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Sequential updates&lt;/strong&gt;: New instances are spun up one at a time, with health checks ensuring each instance is functioning properly before proceeding.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Health check validation&lt;/strong&gt;: Only instances that pass their health checks are considered live, reducing the risk of introducing faulty versions.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Automatic rollback&lt;/strong&gt;: In the event of a failure, Docker Rollout triggers a rollback to the last known good version, maintaining service stability.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Zero downtime goal&lt;/strong&gt;: Though there may be brief downtimes in specific scenarios, Docker Rollout aims to keep services uninterrupted.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Flexible configuration&lt;/strong&gt;: It supports custom timeouts, wait times, and health check parameters for diverse deployment needs.&lt;/li&gt;
&lt;/ul&gt;

</description>
        <pubDate>Fri, 13 Sep 2024 14:40:00 +0000</pubDate>
        <link>https://jwillmer.de/blog/tutorial/zero-downtime-deployment-with-docker-rollout</link>
        <guid isPermaLink="true">https://jwillmer.de/blog/tutorial/zero-downtime-deployment-with-docker-rollout</guid>
        
        <category>docker</category>
        
        <category>zero-downtime</category>
        
        <category>deployment</category>
        
        <category>rollback</category>
        
        
        <category>Tutorial</category>
        
      </item>
    
      <item>
        <title>Securely Expose Local Ports with Tailscale Funnel</title>
        <description>&lt;p&gt;When developing applications that need to interact with external services such as OAuth providers or webhooks, it’s often necessary to expose your local environment to the internet. Tailscale Funnel provides a quick, secure, and hassle-free method to do this, allowing any port on your local machine to be accessible over the internet with minimal configuration. This guide will walk you through setting up Tailscale Funnel to expose your application’s port, making it ideal for developers who need a temporary public endpoint for testing.&lt;/p&gt;

&lt;h3 id=&quot;what-is-tailscale-funnel&quot;&gt;What is Tailscale Funnel?&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://tailscale.com/kb/1223/funnel&quot;&gt;&lt;strong&gt;Tailscale Funnel&lt;/strong&gt;&lt;/a&gt; allows you to expose a local service running on your machine to the internet via HTTPS, leveraging Tailscale’s secure VPN. This feature is especially useful for scenarios where you need a public endpoint, such as testing OAuth callbacks, receiving webhooks, or sharing a development environment temporarily.&lt;/p&gt;

&lt;h3 id=&quot;key-benefits-of-tailscale-funnel&quot;&gt;Key Benefits of Tailscale Funnel&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Quick Setup&lt;/strong&gt;: The process is straightforward and quick, making it perfect for rapid testing and debugging.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Secure and Easily Disabled&lt;/strong&gt;: Public access can be enabled or disabled with a single command, ensuring your local environment remains secure when not in use.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Automatic HTTPS and MagicDNS&lt;/strong&gt;: Tailscale handles HTTPS provisioning and DNS management, simplifying the setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;

&lt;p&gt;To use Tailscale Funnel, you need the following:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Tailscale account&lt;/strong&gt;: Sign up at &lt;a href=&quot;https://tailscale.com&quot;&gt;Tailscale&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Tailscale client&lt;/strong&gt;: Installed and running on your development machine.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;An application running locally&lt;/strong&gt;: Your application should be running on a specific port (e.g., &lt;code&gt;localhost:5000&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;step-by-step-guide-to-set-up-tailscale-funnel&quot;&gt;Step-by-Step Guide to Set Up Tailscale Funnel&lt;/h3&gt;

&lt;h4 id=&quot;1-install-and-configure-tailscale&quot;&gt;1. Install and Configure Tailscale&lt;/h4&gt;

&lt;p&gt;Make sure Tailscale is installed and configured on your machine:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Download and install the Tailscale client&lt;/strong&gt; from &lt;a href=&quot;https://tailscale.com/download&quot;&gt;Tailscale Downloads&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Log in&lt;/strong&gt; to your Tailscale account using the Tailscale client.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Authorize your device&lt;/strong&gt; in the Tailscale admin console.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;2-provision-a-certificate-for-your-device&quot;&gt;2. Provision a Certificate for Your Device&lt;/h4&gt;

&lt;p&gt;To ensure your service is &lt;a href=&quot;https://tailscale.com/kb/1153/enabling-https&quot;&gt;accessible over HTTPS&lt;/a&gt;, you need to provision a certificate for your device using Tailscale:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Open the DNS page&lt;/strong&gt; in the Tailscale admin console.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Enable MagicDNS&lt;/strong&gt; if it is not already enabled for your tailnet.&lt;/li&gt;
  &lt;li&gt;Under &lt;strong&gt;HTTPS Certificates&lt;/strong&gt;, select &lt;strong&gt;Enable HTTPS&lt;/strong&gt;.&lt;/li&gt;
  &lt;li&gt;To obtain a certificate on your machine run the following command in the terminal:&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tailscale cert
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This step is crucial as it enables secure connections to your public endpoint through HTTPS.&lt;/p&gt;

&lt;h4 id=&quot;3-configure-access-controls&quot;&gt;3. Configure Access Controls&lt;/h4&gt;

&lt;p&gt;To enable Funnel, you need to adjust your Tailscale network’s access control list (ACL) settings:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Open the Tailscale admin console&lt;/strong&gt; and go to &lt;strong&gt;Access Controls&lt;/strong&gt;.&lt;/li&gt;
  &lt;li&gt;Modify your ACL configuration to include the Funnel attribute:&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;nodeAttrs&quot;: [
    // Adds the &quot;funnel&quot; attribute to all devices in your network
    { &quot;target&quot;: [&quot;autogroup:member&quot;], &quot;attr&quot;: [&quot;funnel&quot;] }
  ],
  &quot;acls&quot;: [
    // Allow all connections.
    { &quot;action&quot;: &quot;accept&quot;, &quot;src&quot;: [&quot;*&quot;], &quot;dst&quot;: [&quot;*:*&quot;] },
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&quot;4-enable-tailscale-funnel-for-your-application&quot;&gt;4. Enable Tailscale Funnel for Your Application&lt;/h4&gt;

&lt;p&gt;Now, enable Funnel to expose your application:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tailscale funnel &amp;lt;port&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This command will make your application accessible via a public URL over HTTPS.&lt;/p&gt;

&lt;h4 id=&quot;4-obtain-and-test-your-public-url&quot;&gt;4. Obtain and Test Your Public URL&lt;/h4&gt;

&lt;p&gt;Once Funnel is enabled, Tailscale generates a public URL for your service. It will be in the format:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;https://&amp;lt;device-name&amp;gt;.ts.net:&amp;lt;port&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For instance, if your device name is &lt;strong&gt;&lt;code&gt;my-laptop&lt;/code&gt;&lt;/strong&gt; and your application is running on port &lt;strong&gt;&lt;code&gt;5000&lt;/code&gt;&lt;/strong&gt;, the URL will be:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;https://my-laptop.ts.net:5000
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&quot;7-use-the-url-for-external-integrations&quot;&gt;7. Use the URL for External Integrations&lt;/h4&gt;

&lt;p&gt;With your application now accessible online, you can:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Test OAuth callbacks&lt;/strong&gt;: Configure your OAuth provider’s redirect URI to your Tailscale Funnel URL (e.g., &lt;code&gt;https://my-laptop.ts.net:5000/callback&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Receive webhooks&lt;/strong&gt;: Set the Funnel URL as the endpoint for services needing to send data to your application.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Collaborate easily&lt;/strong&gt;: Share your development environment securely for team testing or demonstrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;8-disable-funnel-when-done&quot;&gt;8. Disable Funnel When Done&lt;/h4&gt;

&lt;p&gt;After testing, you can easily disable Funnel to secure your environment:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Open your terminal&lt;/strong&gt; or command prompt.&lt;/li&gt;
  &lt;li&gt;Run the command to disable Funnel:&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tailscale funnel disable &amp;lt;port&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;nice-to-know-using-tailscale-funnel-to-share-files&quot;&gt;Nice to Know: Using Tailscale Funnel to Share Files&lt;/h3&gt;

&lt;p&gt;In addition to exposing your local development environment, Tailscale Funnel can be used to &lt;a href=&quot;https://tailscale.com/kb/1247/funnel-examples&quot;&gt;share files quickly&lt;/a&gt; over the internet. This is particularly useful when you need to send files directly from your device without using an external file-sharing service. Here’s how to use Funnel for file sharing:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tailscale funnel /tmp/public
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This command will activate file sharing mode, and Tailscale will automatically generate a public URL for you.&lt;/p&gt;

</description>
        <pubDate>Tue, 03 Sep 2024 15:35:00 +0000</pubDate>
        <link>https://jwillmer.de/blog/tutorial/securely-expose-local-ports-for-testing-with-tailscale-funnel</link>
        <guid isPermaLink="true">https://jwillmer.de/blog/tutorial/securely-expose-local-ports-for-testing-with-tailscale-funnel</guid>
        
        <category>tailscale</category>
        
        <category>funnel</category>
        
        <category>development</category>
        
        <category>networking</category>
        
        <category>testing</category>
        
        <category>app</category>
        
        
        <category>Tutorial</category>
        
      </item>
    
      <item>
        <title>UXG-Lite router and Vigor-167 modem configuration</title>
        <description>&lt;p&gt;I’m using the UniFi &lt;a href=&quot;https://eu.store.ui.com/eu/en/pro/category/all-cloud-keys-gateways/products/uxg-lite&quot;&gt;UXG-Lite&lt;/a&gt; as a router in my home network. It connects to my DrayTek &lt;a href=&quot;https://www.draytek.com/products/vigor167/&quot;&gt;Vigor 167&lt;/a&gt; modem (previously &lt;a href=&quot;https://www.draytek.com/products/vigor130/&quot;&gt;Vigor 130&lt;/a&gt;). In this post I publish the settings to configure the two devices and describe how you can reach the modem once the setup is running.&lt;/p&gt;

&lt;h2 id=&quot;modem-configuration&quot;&gt;Modem configuration&lt;/h2&gt;
&lt;p&gt;My configuration is Telekom specific. Especially the VDSL2 Tag &lt;code&gt;7&lt;/code&gt; can be different depending on the provider. I configured it on the modem, but it can also be configured on the router. It does not make a difference. The Modem has a static IPv4 assigned: &lt;code&gt;192.168.0.1&lt;/code&gt;&lt;/p&gt;

&lt;div class=&quot;album&quot;&gt;

&lt;figure&gt;
&lt;img src=&quot;https://jwillmer.de/media/img/2024-01-10-uxg-lite-router-vigor-167-modem-configuration-mpoa-settings.png&quot; /&gt;
&lt;figcaption&gt;MPoA Settings&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;
&lt;img src=&quot;https://jwillmer.de/media/img/2024-01-10-uxg-lite-router-vigor-167-modem-configuration-pppoe-settings.png&quot; /&gt;
&lt;figcaption&gt;PPPoE Settings&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;
&lt;img src=&quot;https://jwillmer.de/media/img/2024-01-10-uxg-lite-router-vigor-167-modem-configuration-wan1-ipv6-settings.png&quot; /&gt;
&lt;figcaption&gt;IPv6 Settings&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;
&lt;img src=&quot;https://jwillmer.de/media/img/2024-01-10-uxg-lite-router-vigor-167-modem-configuration-wan1-vdsl2-settings.png&quot; /&gt;
&lt;figcaption&gt;WAN Settings&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;
&lt;img src=&quot;https://jwillmer.de/media/img/2024-01-10-uxg-lite-router-vigor-167-modem-configuration-physical-connection.png&quot; /&gt;
&lt;figcaption&gt;Physical Connection Overview&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;
&lt;img src=&quot;https://jwillmer.de/media/img/2024-01-10-uxg-lite-router-vigor-167-modem-configuration-lan-settings.png&quot; /&gt;
&lt;figcaption&gt;Physical Connection Overview&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;/div&gt;

&lt;h2 id=&quot;router-configuration&quot;&gt;Router Configuration&lt;/h2&gt;
&lt;p&gt;The internet configuration on the router is a no brainer. Open the Internet settings and if you haven’t configured the VLAN ID on the modem you have to select it in the router. In the IPv4 configuration section you have to select PPPoE and input username and password that was provided by your ISP (Telekom).&lt;/p&gt;

&lt;h3 id=&quot;telekom-username&quot;&gt;Telekom username&lt;/h3&gt;
&lt;p&gt;The construction of the username is very cumbersome. Below is the format you have to generate:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Anschlusskennung: 111111111111
Teilnehmernummer: 222222222222 (12 digits) or 33333333333 (11 digits)
Mitbenutzerkennung: 0001

Teilnehmernummer with 12 digits example: 1111111111112222222222220001@t-online.de
Teilnehmernummer with 11 digits example: 11111111111133333333333#0001@t-online.de
                                         11111111111133333333333\#0001@t-online.de
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;connecting-to-the-modem&quot;&gt;Connecting to the Modem&lt;/h2&gt;
&lt;p&gt;Once everything is configured and in place the modem is not reachable anymore. On a UXG Gateway you can create the following configuration to make it work:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;interfaces&quot;: {
        &quot;pseudo-ethernet&quot;: {
            &quot;peth0&quot;: {
                &quot;address&quot;: [&quot;192.168.0.2/24&quot;],
                &quot;description&quot;: &quot;Access to Modem&quot;,
                &quot;link&quot;: [&quot;eth1&quot;]
            }
        }
    },
    &quot;service&quot;: {
        &quot;nat&quot;: {
            &quot;rule&quot;: {
                &quot;5000&quot;: {
                    &quot;description&quot;: &quot;MASQ Modem access&quot;,
                    &quot;destination&quot;: {
                        &quot;address&quot;: [&quot;192.168.0.1&quot;]
                    },
                    &quot;outbound-interface&quot;: [&quot;peth0&quot;],
                    &quot;type&quot;: &quot;masquerade&quot;
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;However, the UXG-Lite does not have the option to configure it permanently. What you can do instead is to login to the UXG-Lite via SSH and add the routing for eth1 manually:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Assign a Static IP to eth1
sudo ip addr add 192.168.0.2/24 dev eth1

# Bring the Interface Up
sudo ip link set eth1 up

# Add a static route
sudo ip route add 192.168.0.0/24 dev eth1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After that you can create a SSH tunnel to your UXG-Lite and setup a SOCKS5 proxy in your browser in order to reach the Modem.&lt;/p&gt;

&lt;figure&gt;
   &lt;img src=&quot;https://jwillmer.de/media/img/2024-01-10-uxg-lite-router-vigor-167-modem-configuration-putty-tunnel.png&quot; /&gt;
   &lt;figcaption&gt;Putty tunnel&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;
   &lt;img src=&quot;https://jwillmer.de/media/img/2024-01-10-uxg-lite-router-vigor-167-modem-configuration-firefox-socks5-proxy.png&quot; /&gt;
   &lt;figcaption&gt;Firefox SOCKS5 proxy&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;revert-the-changes-to-the-routes&quot;&gt;Revert the changes to the routes&lt;/h3&gt;
&lt;p&gt;This is not strictly necessary. After a reboot of the device the settings are lost. I also noticed that any modification to the WAN settings on the UXG-Lite will remove the route. However, if you like to manually remove it after you are done you can execute the following command:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo ip route del 192.168.0.0/24
&lt;/code&gt;&lt;/pre&gt;

</description>
        <pubDate>Wed, 10 Jan 2024 20:10:00 +0000</pubDate>
        <link>https://jwillmer.de/blog/tutorial/uxg-lite-router-vigor-167-modem-configuration</link>
        <guid isPermaLink="true">https://jwillmer.de/blog/tutorial/uxg-lite-router-vigor-167-modem-configuration</guid>
        
        <category>UXG-Lite</category>
        
        <category>gateway</category>
        
        <category>modem</category>
        
        <category>router</category>
        
        <category>connectivity</category>
        
        
        <category>Tutorial</category>
        
      </item>
    
  </channel>
</rss>
