<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>A Developer&#39;s Journey</title>
  
  <subtitle>Code &amp; Conquer</subtitle>
  <link href="https://devapro.github.io/atom.xml" rel="self"/>
  
  <link href="https://devapro.github.io/"/>
  <updated>2026-06-28T15:21:53.868Z</updated>
  <id>https://devapro.github.io/</id>
  
  <author>
    <name>Arsenii Kharlanov</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Anatomy of an Agent-Ready Repo: Skills, Rules, and Docs That Live With the Code</title>
    <link href="https://devapro.github.io/en/2026/06/10/anatomy-of-an-agent-ready-repo/"/>
    <id>https://devapro.github.io/en/2026/06/10/anatomy-of-an-agent-ready-repo/</id>
    <published>2026-06-10T10:00:00.000Z</published>
    <updated>2026-06-28T15:21:53.868Z</updated>
    
    <content type="html"><![CDATA[<p>My two earlier posts argued the <em>why</em>: <a href="/en/2026/06/27/documentation-in-the-ai-era/">Documentation in the AI Era</a> made the case that docs are now read by an LLM on every task, and <a href="/en/2026/06/27/documentation-driven-development/">D3</a> turned that into a development process. This post is the <em>what</em> — a walk through an actual repository that’s been laid out for an agent, file by file, with one question asked of every layer: <strong>what can the AI now do that it couldn’t before?</strong></p><p>The subject is a production Android app: 100+ Gradle modules, a feature-per-module architecture, two product flavors. Big enough that no human holds it all in their head — which is exactly the condition under which an agent’s ability to <em>navigate without guessing</em> stops being a nicety and becomes the whole game.</p><p>The repo has two halves that do two different jobs:</p><ul><li><strong><code>.claude/</code></strong> tells the agent <em>how to work here</em> — the conventions, the procedures, the review standards.</li><li><strong><code>docs/</code></strong> tells the agent <em>what the system does</em> — the behavior specs, the module maps, the analytics contracts.</li></ul><p>Neither is useful without the other. A skill that knows <em>how</em> to implement a feature still has to read a doc to learn <em>what</em> the feature is. Let’s take them in turn.</p><h2 id="Half-One-claude-—-How-the-Agent-Works"><a href="#Half-One-claude-—-How-the-Agent-Works" class="headerlink" title="Half One: .claude/ — How the Agent Works"></a>Half One: <code>.claude/</code> — How the Agent Works</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">.claude/</span><br><span class="line">├── rules/      17 topical convention files  (the rubric)</span><br><span class="line">├── skills/     24 named workflows           (the procedures)</span><br><span class="line">├── agents/     10 specialist sub-agents     (the reviewers)</span><br><span class="line">├── hooks/      shell hooks on edit/stop     (the guardrails)</span><br><span class="line">└── scripts/    deterministic helpers        (the non-AI work)</span><br></pre></td></tr></table></figure><h3 id="Rules-—-the-team’s-conventions-as-a-checklist"><a href="#Rules-—-the-team’s-conventions-as-a-checklist" class="headerlink" title="Rules — the team’s conventions as a checklist"></a>Rules — the team’s conventions as a checklist</h3><p><code>.claude/rules/*.md</code> are narrow, single-topic files: <code>mvi-architecture.md</code>, <code>navigation.md</code>, <code>app-result.md</code>, <code>webview.md</code>, <code>analytics.md</code>, and so on. Each one codifies <em>one</em> convention, and the format matters more than you’d expect. They aren’t prose essays — they’re rubrics, with ✅ correct &#x2F; ❌ wrong pairs the agent can pattern-match against.</p><p>Here’s the real <code>app-result.md</code>, which governs how the codebase chains fallible operations:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ WRONG: map with an AppResult-returning lambda → AppResult&lt;AppResult&lt;Profile&gt;&gt;</span></span><br><span class="line"><span class="keyword">val</span> nested = result.map &#123; user -&gt; fetchUserProfile(user.id) &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ CORRECT: flatMap flattens the nested result</span></span><br><span class="line"><span class="keyword">val</span> profile = result.flatMap &#123; user -&gt; fetchUserProfile(user.id) &#125;</span><br></pre></td></tr></table></figure><p><strong>Capability unlocked: the AI writes code that matches <em>your</em> conventions, not the statistical average of GitHub.</strong> Without this file, a model produces idiomatic-but-generic Kotlin. With it, the model produces code that looks like the rest of <em>this</em> repo — internal visibility, no public constants, <code>flatMap</code> over manual <code>when</code>. The convention travels with the code, so it’s correct on every run, for every contributor, human or model.</p><p>The clever part is the feedback loop: a skill called <code>/review-and-rule</code> watches how the codebase actually handles a pattern, then <em>writes or updates the rule file itself</em>. Conventions discovered in review become rules enforced in the next implementation. The rubric maintains itself.</p><h3 id="Skills-—-procedures-not-prompts"><a href="#Skills-—-procedures-not-prompts" class="headerlink" title="Skills — procedures, not prompts"></a>Skills — procedures, not prompts</h3><p>A skill is a named, multi-phase workflow invoked with <code>/skill-name</code>. This repo has 24 of them. They’re the difference between asking an agent to “implement a feature” and handing it the team’s actual playbook for doing so.</p><p><code>/implement-feature</code> is representative. It doesn’t just write code — it runs phases:</p><ol><li><strong>Interview</strong> — asks a fixed set of requirement questions via structured prompts.</li><li><strong>Design</strong> — derives the MVI shape (State &#x2F; Action &#x2F; Event &#x2F; Reducer) from the answers.</li><li><strong>Scaffold</strong> — generates the <code>api-*</code> + <code>feature-*</code> module pair.</li><li><strong>Implement</strong> — writes real logic and Compose UI following the rules above.</li><li><strong>Self-review</strong> — invokes the <code>code-reviewer</code> sub-agent before handing back.</li></ol><p>Others are sharper tools: <code>/task-worker</code> takes a Jira ticket and runs it end-to-end with up to three auto-repair loops; <code>/check-coverage</code> runs JaCoCo, finds the gaps, and writes tests in batches until a target is met; <code>/discovery-to-srs</code> turns a rough draft into a structured spec and has an independent agent validate it.</p><p><strong>Capability unlocked: the AI executes a process instead of improvising one.</strong> Improvisation is where agents drift — every run reinvents the approach, and quality is a coin flip. A skill pins the <em>steps</em> while leaving the <em>content</em> to the model. The result is repeatability: <code>/implement-feature</code> produces the same module shape today as it did last month, because the phases are fixed even though the code is new.</p><h3 id="Sub-agents-—-specialists-with-fresh-context-and-a-narrow-rubric"><a href="#Sub-agents-—-specialists-with-fresh-context-and-a-narrow-rubric" class="headerlink" title="Sub-agents — specialists with fresh context and a narrow rubric"></a>Sub-agents — specialists with fresh context and a narrow rubric</h3><p><code>.claude/agents/</code> holds 10 sub-agent definitions, most of them reviewers: <code>pr-review-architecture</code>, <code>pr-review-compose</code>, <code>pr-review-performance</code>, <code>pr-review-tests</code>, <code>pr-review-package-structure</code>, <code>pr-review-code-quality</code>. The <code>/review-pr-advanced</code> skill fans all six out <strong>in parallel</strong>, each in its own context window, each handed exactly one job.</p><p>The architecture reviewer’s prompt is a tight checklist — “Reducers call Use Cases, not Repositories”, “every implementation class must be <code>internal</code>“, “navigation goes through a Router interface” — and it’s explicitly told <em>not</em> to review formatting or Compose perf, because other agents own those.</p><p><strong>Capability unlocked: depth without dilution.</strong> A single agent asked to “review this PR” spreads its attention thin and its context fills with noise. Six specialists, each with a focused rubric and a clean context, each catch things a generalist misses — and they run concurrently, so the wall-clock cost is one review, not six. This is the <em>find → specialize → verify</em> shape that one big prompt can’t replicate.</p><h3 id="Hooks-and-scripts-—-the-parts-that-shouldn’t-be-AI"><a href="#Hooks-and-scripts-—-the-parts-that-shouldn’t-be-AI" class="headerlink" title="Hooks and scripts — the parts that shouldn’t be AI"></a>Hooks and scripts — the parts that shouldn’t be AI</h3><p>Two more layers round it out. <strong>Hooks</strong> (<code>post-edit-review-reminder.sh</code>, <code>pre-stop-review-check.sh</code>) are shell scripts the harness fires on edit and before the agent stops — they nudge the review step so it can’t be silently skipped. <strong>Scripts</strong> (the JaCoCo XML parsers, the doc-index generators) are plain Python: deterministic work that would be wasteful and unreliable to do with a model.</p><p><strong>Capability unlocked: the agent knows what <em>not</em> to think about.</strong> Parsing an XML coverage report doesn’t need a language model; enforcing “review before you stop” shouldn’t depend on the model remembering to. Pushing this work into hooks and scripts makes the system both cheaper and more reliable — the AI spends its tokens on judgment, not bookkeeping.</p><h2 id="Half-Two-docs-—-What-the-System-Does"><a href="#Half-Two-docs-—-What-the-System-Does" class="headerlink" title="Half Two: docs/ — What the System Does"></a>Half Two: <code>docs/</code> — What the System Does</h2><p>The product documentation lives in a <strong>nested git repository</strong> cloned at <code>docs/</code>, with the actual content under <code>docs/docs/</code>. That nesting is deliberate (more on <em>why in the repo</em> below), but the structure inside is the interesting part:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">docs/docs/</span><br><span class="line">├── INDEX.md          module → feature lookup  (generated)</span><br><span class="line">├── llms.txt          curated manifest for agents</span><br><span class="line">├── STYLE.md          machine-checkable doc conventions</span><br><span class="line">├── GLOSSARY.md       acronyms (WL, LP, SRS, TNB…)</span><br><span class="line">├── features/</span><br><span class="line">│   └── &lt;Feature&gt;/</span><br><span class="line">│       ├── README.md         folder index    (generated)</span><br><span class="line">│       ├── modules.md        module map       (source of truth)</span><br><span class="line">│       ├── &lt;Feature&gt;.srs.md  the primary spec</span><br><span class="line">│       ├── analytics/        event specs</span><br><span class="line">│       ├── _drafts/          WIP  (status: draft)</span><br><span class="line">│       └── _archive/         deprecated</span><br><span class="line">└── technical/        cross-cutting reference</span><br></pre></td></tr></table></figure><h3 id="INDEX-md-—-the-lookup-table-that-kills-blind-search"><a href="#INDEX-md-—-the-lookup-table-that-kills-blind-search" class="headerlink" title="INDEX.md — the lookup table that kills blind search"></a>INDEX.md — the lookup table that kills blind search</h3><p><code>INDEX.md</code> maps every one of the ~265 code modules to the feature it belongs to:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">| `:features:feature-alerts-feed`       | Alerts            |</span><br><span class="line">| `:features:feature-instrument-tab-news` | Instrument screen |</span><br><span class="line">| `:services:service-deep-links`        | Deep links        |</span><br></pre></td></tr></table></figure><p>Crucially, the module → feature section is <strong>generated</strong> from each feature’s <code>modules.md</code> by a Python script — so it can’t drift from reality by hand.</p><p><strong>Capability unlocked: the agent’s first move is a lookup, not a search.</strong> Drop the model into a 100-module repo with no index and “where does the alerts feed live?” becomes a fan-out of greps and guesses. With <code>INDEX.md</code>, it’s one hop: module → feature folder → spec. Every skill in the repo starts from this same hop. The 45 feature folders mean the difference between an agent that explores and an agent that <em>retrieves</em>.</p><h3 id="llms-txt-and-STYLE-md-—-a-contract-for-machine-readers"><a href="#llms-txt-and-STYLE-md-—-a-contract-for-machine-readers" class="headerlink" title="llms.txt and STYLE.md — a contract for machine readers"></a>llms.txt and STYLE.md — a contract for machine readers</h3><p><code>llms.txt</code> is the <a href="https://llmstxt.org/">emerging convention</a> for an AI entry point: a curated manifest pointing at each feature’s <em>primary</em> spec, with drafts and archives deliberately excluded so the agent reads the authoritative doc first.</p><p><code>STYLE.md</code> is the part I find most underrated. It’s a style guide that’s actually <em>enforceable</em>: every doc must declare a <code>type</code> from a fixed enum (<code>srs</code>, <code>overview</code>, <code>api</code>, <code>analytics</code>…), every spec must cover the three user states (guest &#x2F; registered &#x2F; subscribed), filenames follow a canonical pattern — and a CI gate (<code>check-doc-filenames.py</code>, <code>run-quality-gates.py</code>) fails the build when they don’t.</p><p><strong>Capability unlocked: AI-<em>written</em> docs come out uniform.</strong> When <code>/discovery-to-srs</code> or the Jira pipeline generates a spec, <code>STYLE.md</code> is the schema it writes against — so machine-authored docs land in the same shape as hand-written ones, and the next agent that reads them knows exactly where to look. A style guide that a human merely <em>reads</em> is advisory; one that’s an enum plus a CI gate is a contract both sides honor.</p><h3 id="modules-md-and-the-lifecycle-folders-—-honesty-over-time"><a href="#modules-md-and-the-lifecycle-folders-—-honesty-over-time" class="headerlink" title="modules.md and the lifecycle folders — honesty over time"></a>modules.md and the lifecycle folders — honesty over time</h3><p>Each feature’s <code>modules.md</code> is the <strong>source of truth</strong> for which code belongs to it, and the rule is that adding a module in code means updating <code>modules.md</code> in the <em>same PR</em>. The <code>_drafts/</code> and <code>_archive/</code> subfolders, gated by a <code>status</code> field, keep work-in-progress and dead docs from polluting what the agent treats as authoritative.</p><p><strong>Capability unlocked: the agent can trust what it reads.</strong> The single most expensive failure mode for doc-driven AI is confidently acting on a stale spec — it never throws an error, it just does the wrong thing. Same-PR module updates and explicit lifecycle status are the cheap, unglamorous machinery that keeps “the docs say X” and “the code does X” from diverging.</p><h2 id="The-Connective-Tissue"><a href="#The-Connective-Tissue" class="headerlink" title="The Connective Tissue"></a>The Connective Tissue</h2><p>The two halves meet in a single invariant that every skill follows: <strong>resolve, read, then act.</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Jira ticket  →  INDEX.md (module → feature)</span><br><span class="line">             →  modules.md (which modules)</span><br><span class="line">             →  &lt;Feature&gt;.srs.md (documented behavior)</span><br><span class="line">             →  implement, guided by .claude/rules/*</span><br><span class="line">             →  review via specialist sub-agents</span><br><span class="line">             →  /update-docs flags which specs drifted</span><br></pre></td></tr></table></figure><p><code>task-worker</code> lives this loop end to end. It never guesses where a feature is or how it should behave — it looks both up, builds against the documented contract, implements under the rules, reviews with the specialists, and then checks whether its own changes made the docs stale. The systems from the first post — the Doc-Chat Q&amp;A bot, the Jira→Docs pipeline — are just <em>more consumers</em> of this same structure. They work because the index, the specs, and the rules already exist for them to read.</p><h2 id="What-the-AI-Can-Do-Now-—-The-Short-Version"><a href="#What-the-AI-Can-Do-Now-—-The-Short-Version" class="headerlink" title="What the AI Can Do Now — The Short Version"></a>What the AI Can Do Now — The Short Version</h2><table><thead><tr><th>Layer</th><th>Capability it unlocks</th></tr></thead><tbody><tr><td><code>.claude/rules/</code></td><td>Writes code in <em>your</em> conventions, not the GitHub average</td></tr><tr><td><code>.claude/skills/</code></td><td>Runs a repeatable process instead of improvising one</td></tr><tr><td><code>.claude/agents/</code></td><td>Deep, parallel review — specialists, not one tired generalist</td></tr><tr><td><code>.claude/hooks</code> + <code>scripts/</code></td><td>Spends tokens on judgment; offloads bookkeeping</td></tr><tr><td><code>docs/INDEX.md</code></td><td>Retrieves by lookup instead of searching blind</td></tr><tr><td><code>docs/llms.txt</code> + <code>STYLE.md</code></td><td>Reads the right doc; writes new ones in a uniform shape</td></tr><tr><td><code>modules.md</code> + lifecycle</td><td>Trusts what it reads — docs don’t silently rot</td></tr></tbody></table><h2 id="Why-In-the-Repo-Specifically"><a href="#Why-In-the-Repo-Specifically" class="headerlink" title="Why In the Repo Specifically"></a>Why <em>In the Repo</em> Specifically</h2><p>Every benefit above depends on this context living in the repository, not in Confluence or a wiki. Four reasons, all of which compound:</p><ul><li><strong>Auto-loaded.</strong> <code>CLAUDE.md</code> and <code>.claude/</code> are pulled into every agent session with zero setup. A wiki page has to be found, fetched, and pasted — which means it usually isn’t.</li><li><strong>Version-locked.</strong> The docs are at the same commit as the code. Check out a branch from six months ago and you get <em>that</em> branch’s specs and rules — no “which version of this Confluence page matches this tag?” guesswork.</li><li><strong>Same diff.</strong> A behavior change and its doc update ride the same PR, reviewed together. <code>/update-docs</code> exists precisely to keep that link tight. Confluence updates happen weeks later, if ever.</li><li><strong>Greppable and cache-friendly.</strong> Plain text on the live filesystem means an agent can <code>Grep</code> the <em>current</em> state — no embedding index to re-sync (the no-vector-DB argument from the first post). And low-churn plain text keeps the prompt cache warm, which is a real cost line at scale.</li></ul><h2 id="Bottom-Line"><a href="#Bottom-Line" class="headerlink" title="Bottom Line"></a>Bottom Line</h2><p>An agent-ready repo isn’t one big clever prompt. It’s two boring, layered halves: a <code>.claude/</code> that encodes <em>how the team works</em> and a <code>docs/</code> that encodes <em>what the system does</em> — joined by the discipline of <em>resolve, read, act</em>. Each file is small and unglamorous on its own; together they’re the difference between an AI that explores your codebase and one that <em>operates</em> it.</p><p>The investment is real, but it’s the same investment that makes a codebase legible to a new senior engineer — and now you’re making it once for every future agent run as well. Start with the three files from the first post (<code>CLAUDE.md</code>, a few rules, an index), ship one skill, and let <code>/review-and-rule</code> grow the rest from what your reviews already know.</p><h3 id="Further-Reading"><a href="#Further-Reading" class="headerlink" title="Further Reading"></a>Further Reading</h3><ul><li><a href="/en/2026/06/27/documentation-in-the-ai-era/">Documentation in the AI Era: Docs Are the New Source Code</a> — the systems built on top of this structure</li><li><a href="/en/2026/06/27/documentation-driven-development/">D3: Documentation Driven Development</a> — the process that produces the specs</li><li>Anthropic — <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code best practices</a> and <a href="https://code.claude.com/">Agent Skills</a></li><li><a href="https://llmstxt.org/">llms.txt</a> — the machine-readable entry-point convention</content></invoke></li></ul>]]></content>
    
    
    <summary type="html">A guided tour of a real 100+ module Android repo laid out for an LLM agent — the `.claude/` skills, rules, and sub-agents that tell it *how* to work, and the in-repo `docs/` that tell it *what* the system does — and the concrete capability each layer unlocks.</summary>
    
    
    
    <category term="AI" scheme="https://devapro.github.io/categories/AI/"/>
    
    
    <category term="android" scheme="https://devapro.github.io/tags/android/"/>
    
    <category term="ai" scheme="https://devapro.github.io/tags/ai/"/>
    
    <category term="documentation" scheme="https://devapro.github.io/tags/documentation/"/>
    
    <category term="llm" scheme="https://devapro.github.io/tags/llm/"/>
    
    <category term="claude" scheme="https://devapro.github.io/tags/claude/"/>
    
    <category term="agents" scheme="https://devapro.github.io/tags/agents/"/>
    
    <category term="skills" scheme="https://devapro.github.io/tags/skills/"/>
    
    <category term="developer-experience" scheme="https://devapro.github.io/tags/developer-experience/"/>
    
  </entry>
  
  <entry>
    <title>Анатомия репозитория, готового для ИИ-агента: навыки, правила и документация рядом с кодом</title>
    <link href="https://devapro.github.io/ru/2026/06/10/anatomy-of-an-agent-ready-repo-ru/"/>
    <id>https://devapro.github.io/ru/2026/06/10/anatomy-of-an-agent-ready-repo-ru/</id>
    <published>2026-06-10T10:00:00.000Z</published>
    <updated>2026-06-28T15:21:53.868Z</updated>
    
    <content type="html"><![CDATA[<p>Два предыдущих поста объясняли <em>почему</em>: <a href="/ru/2026/06/27/documentation-in-the-ai-era/">Документация в эпоху ИИ</a> показал, что теперь документацию читает LLM при каждой задаче, а <a href="/ru/2026/06/27/documentation-driven-development/">D3</a> превратил это в процесс разработки. Этот пост — про <em>что именно</em>: проход по реальному репозиторию, обустроенному под агента, файл за файлом, с одним вопросом к каждому слою — <strong>что теперь умеет ИИ, чего не мог раньше?</strong></p><p>Объект — продакшен Android-приложение: 100+ Gradle-модулей, архитектура «модуль на фичу», два продуктовых флейвора. Достаточно большое, чтобы никто не держал его целиком в голове, — а именно в этих условиях способность агента <em>ориентироваться без догадок</em> перестаёт быть приятным бонусом и становится сутью дела.</p><p>У репозитория две половины, решающие две разные задачи:</p><ul><li><strong><code>.claude/</code></strong> говорит агенту, <em>как здесь работать</em>, — соглашения, процедуры, стандарты ревью.</li><li><strong><code>docs/</code></strong> говорит агенту, <em>что делает система</em>, — спецификации поведения, карты модулей, контракты аналитики.</li></ul><p>Ни одна не работает без другой. Навык, знающий, <em>как</em> реализовать фичу, всё равно должен прочитать документ, чтобы узнать, <em>что</em> это за фича. Разберём по порядку.</p><h2 id="Половина-первая-claude-—-как-агент-работает"><a href="#Половина-первая-claude-—-как-агент-работает" class="headerlink" title="Половина первая: .claude/ — как агент работает"></a>Половина первая: <code>.claude/</code> — как агент работает</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">.claude/</span><br><span class="line">├── rules/      17 тематических файлов соглашений  (рубрика)</span><br><span class="line">├── skills/     24 именованных воркфлоу             (процедуры)</span><br><span class="line">├── agents/     10 специализированных суб-агентов   (ревьюеры)</span><br><span class="line">├── hooks/      shell-хуки на edit/stop             (страховка)</span><br><span class="line">└── scripts/    детерминированные помощники         (не-ИИ работа)</span><br></pre></td></tr></table></figure><h3 id="Правила-—-соглашения-команды-в-виде-чек-листа"><a href="#Правила-—-соглашения-команды-в-виде-чек-листа" class="headerlink" title="Правила — соглашения команды в виде чек-листа"></a>Правила — соглашения команды в виде чек-листа</h3><p><code>.claude/rules/*.md</code> — узкие однотемные файлы: <code>mvi-architecture.md</code>, <code>navigation.md</code>, <code>app-result.md</code>, <code>webview.md</code>, <code>analytics.md</code> и так далее. Каждый кодифицирует <em>одно</em> соглашение, и формат важнее, чем кажется. Это не эссе — это рубрики с парами ✅ верно &#x2F; ❌ неверно, с которыми агент сверяется по образцу.</p><p>Вот реальный <code>app-result.md</code>, описывающий, как в кодовой базе сцепляются операции, способные упасть:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ НЕВЕРНО: map с лямбдой, возвращающей AppResult → AppResult&lt;AppResult&lt;Profile&gt;&gt;</span></span><br><span class="line"><span class="keyword">val</span> nested = result.map &#123; user -&gt; fetchUserProfile(user.id) &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ ВЕРНО: flatMap «разворачивает» вложенный результат</span></span><br><span class="line"><span class="keyword">val</span> profile = result.flatMap &#123; user -&gt; fetchUserProfile(user.id) &#125;</span><br></pre></td></tr></table></figure><p><strong>Раскрытая способность: ИИ пишет код по <em>вашим</em> соглашениям, а не по статистическому среднему GitHub.</strong> Без этого файла модель выдаёт идиоматичный, но обобщённый Kotlin. С ним — код, похожий на остальной <em>этот</em> репозиторий: <code>internal</code>-видимость, никаких публичных констант, <code>flatMap</code> вместо ручного <code>when</code>. Соглашение едет вместе с кодом, поэтому оно соблюдается при каждом запуске, для каждого участника — человека или модели.</p><p>Самое изящное — обратная связь: навык <code>/review-and-rule</code> наблюдает, как кодовая база на самом деле обрабатывает паттерн, и <em>сам пишет или обновляет файл правила</em>. Соглашения, найденные на ревью, становятся правилами, применяемыми при следующей реализации. Рубрика поддерживает себя сама.</p><h3 id="Навыки-—-процедуры-а-не-промпты"><a href="#Навыки-—-процедуры-а-не-промпты" class="headerlink" title="Навыки — процедуры, а не промпты"></a>Навыки — процедуры, а не промпты</h3><p>Навык — это именованный многофазный воркфлоу, вызываемый через <code>/skill-name</code>. В этом репозитории их 24. Это и есть разница между «попросить агента реализовать фичу» и «вручить ему реальный регламент команды».</p><p><code>/implement-feature</code> — показательный пример. Он не просто пишет код, а проходит фазы:</p><ol><li><strong>Интервью</strong> — задаёт фиксированный набор вопросов о требованиях через структурированные запросы.</li><li><strong>Дизайн</strong> — выводит из ответов MVI-форму (State &#x2F; Action &#x2F; Event &#x2F; Reducer).</li><li><strong>Каркас</strong> — генерирует пару модулей <code>api-*</code> + <code>feature-*</code>.</li><li><strong>Реализация</strong> — пишет реальную логику и Compose UI по правилам выше.</li><li><strong>Саморевью</strong> — вызывает суб-агента <code>code-reviewer</code> перед возвратом результата.</li></ol><p>Другие навыки острее: <code>/task-worker</code> берёт Jira-тикет и проводит его от начала до конца с тремя циклами авто-починки; <code>/check-coverage</code> запускает JaCoCo, находит пробелы и пишет тесты пачками, пока не достигнет цели; <code>/discovery-to-srs</code> превращает черновик в структурированную спецификацию и отдаёт независимому агенту на валидацию.</p><p><strong>Раскрытая способность: ИИ выполняет процесс, а не импровизирует его.</strong> Импровизация — это там, где агенты «уплывают»: каждый запуск заново изобретает подход, и качество — подбрасывание монетки. Навык фиксирует <em>шаги</em>, оставляя <em>содержание</em> модели. Результат — воспроизводимость: <code>/implement-feature</code> сегодня выдаёт ту же форму модуля, что и месяц назад, потому что фазы фиксированы, хотя код новый.</p><h3 id="Суб-агенты-—-специалисты-со-свежим-контекстом-и-узкой-рубрикой"><a href="#Суб-агенты-—-специалисты-со-свежим-контекстом-и-узкой-рубрикой" class="headerlink" title="Суб-агенты — специалисты со свежим контекстом и узкой рубрикой"></a>Суб-агенты — специалисты со свежим контекстом и узкой рубрикой</h3><p>В <code>.claude/agents/</code> — 10 определений суб-агентов, большинство из них ревьюеры: <code>pr-review-architecture</code>, <code>pr-review-compose</code>, <code>pr-review-performance</code>, <code>pr-review-tests</code>, <code>pr-review-package-structure</code>, <code>pr-review-code-quality</code>. Навык <code>/review-pr-advanced</code> запускает все шесть <strong>параллельно</strong>, каждого в своём контекстном окне, с одной-единственной задачей.</p><p>Промпт архитектурного ревьюера — плотный чек-лист: «Reducer вызывает Use Case, а не Repository», «каждый класс реализации должен быть <code>internal</code>», «навигация идёт через интерфейс Router», — и ему явно сказано <em>не</em> проверять форматирование или производительность Compose, потому что за это отвечают другие агенты.</p><p><strong>Раскрытая способность: глубина без размывания.</strong> Один агент, которого просят «отревьюить этот PR», распыляет внимание, а его контекст забивается шумом. Шесть специалистов, каждый с фокусной рубрикой и чистым контекстом, ловят то, что упускает универсал, — и работают одновременно, так что по времени это одно ревью, а не шесть. Это форма <em>найти → специализировать → проверить</em>, которую один большой промпт не повторит.</p><h3 id="Хуки-и-скрипты-—-то-что-не-должно-быть-ИИ"><a href="#Хуки-и-скрипты-—-то-что-не-должно-быть-ИИ" class="headerlink" title="Хуки и скрипты — то, что не должно быть ИИ"></a>Хуки и скрипты — то, что не должно быть ИИ</h3><p>Ещё два слоя замыкают картину. <strong>Хуки</strong> (<code>post-edit-review-reminder.sh</code>, <code>pre-stop-review-check.sh</code>) — это shell-скрипты, которые харнесс запускает при редактировании и перед остановкой агента; они подталкивают к шагу ревью, чтобы его нельзя было молча пропустить. <strong>Скрипты</strong> (парсеры XML-отчётов JaCoCo, генераторы индексов документации) — обычный Python: детерминированная работа, которую расточительно и ненадёжно делать моделью.</p><p><strong>Раскрытая способность: агент знает, о чём <em>не</em> надо думать.</strong> Разбор XML-отчёта о покрытии не требует языковой модели; соблюдение правила «ревью перед остановкой» не должно зависеть от того, вспомнит ли о нём модель. Вынос этой работы в хуки и скрипты делает систему дешевле и надёжнее — ИИ тратит токены на суждения, а не на бухгалтерию.</p><h2 id="Половина-вторая-docs-—-что-делает-система"><a href="#Половина-вторая-docs-—-что-делает-система" class="headerlink" title="Половина вторая: docs/ — что делает система"></a>Половина вторая: <code>docs/</code> — что делает система</h2><p>Продуктовая документация живёт во <strong>вложенном git-репозитории</strong>, склонированном в <code>docs/</code>, а само содержимое — в <code>docs/docs/</code>. Вложенность сделана осознанно (про <em>почему именно в репозитории</em> — ниже), но интересна как раз структура внутри:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">docs/docs/</span><br><span class="line">├── INDEX.md          модуль → фича  (генерируется)</span><br><span class="line">├── llms.txt          выверенный манифест для агентов</span><br><span class="line">├── STYLE.md          машинно-проверяемые соглашения</span><br><span class="line">├── GLOSSARY.md       акронимы (WL, LP, SRS, TNB…)</span><br><span class="line">├── features/</span><br><span class="line">│   └── &lt;Feature&gt;/</span><br><span class="line">│       ├── README.md         индекс папки    (генерируется)</span><br><span class="line">│       ├── modules.md        карта модулей    (источник истины)</span><br><span class="line">│       ├── &lt;Feature&gt;.srs.md  основная спека</span><br><span class="line">│       ├── analytics/        спеки событий</span><br><span class="line">│       ├── _drafts/          в работе  (status: draft)</span><br><span class="line">│       └── _archive/         устаревшее</span><br><span class="line">└── technical/        сквозной справочник</span><br></pre></td></tr></table></figure><h3 id="INDEX-md-—-таблица-соответствий-убивающая-слепой-поиск"><a href="#INDEX-md-—-таблица-соответствий-убивающая-слепой-поиск" class="headerlink" title="INDEX.md — таблица соответствий, убивающая слепой поиск"></a>INDEX.md — таблица соответствий, убивающая слепой поиск</h3><p><code>INDEX.md</code> сопоставляет каждый из ~265 модулей кода с фичей, к которой он относится:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">| `:features:feature-alerts-feed`         | Alerts            |</span><br><span class="line">| `:features:feature-instrument-tab-news` | Instrument screen |</span><br><span class="line">| `:services:service-deep-links`          | Deep links        |</span><br></pre></td></tr></table></figure><p>Важно: секция «модуль → фича» <strong>генерируется</strong> из <code>modules.md</code> каждой фичи Python-скриптом — поэтому она не может разойтись с реальностью из-за ручной правки.</p><p><strong>Раскрытая способность: первый ход агента — это поиск по индексу, а не перебор.</strong> Бросьте модель в репозиторий из 100 модулей без индекса, и «где живёт лента алертов?» превращается в веер grep’ов и догадок. С <code>INDEX.md</code> — это один прыжок: модуль → папка фичи → спека. Каждый навык в репозитории начинается с этого же прыжка. 45 папок фич — это разница между агентом, который <em>исследует</em>, и агентом, который <em>извлекает</em>.</p><h3 id="llms-txt-и-STYLE-md-—-контракт-для-машинных-читателей"><a href="#llms-txt-и-STYLE-md-—-контракт-для-машинных-читателей" class="headerlink" title="llms.txt и STYLE.md — контракт для машинных читателей"></a>llms.txt и STYLE.md — контракт для машинных читателей</h3><p><code>llms.txt</code> — это <a href="https://llmstxt.org/">формирующееся соглашение</a> о точке входа для ИИ: выверенный манифест, указывающий на <em>основную</em> спеку каждой фичи, причём черновики и архив намеренно исключены, чтобы агент сначала читал авторитетный документ.</p><p><code>STYLE.md</code> — на мой взгляд, самое недооценённое. Это гайд по стилю, который реально <em>проверяем</em>: каждый документ обязан объявить <code>type</code> из фиксированного перечня (<code>srs</code>, <code>overview</code>, <code>api</code>, <code>analytics</code>…), каждая спека обязана покрывать три состояния пользователя (гость &#x2F; зарегистрированный &#x2F; с подпиской), имена файлов следуют канону — и CI-гейт (<code>check-doc-filenames.py</code>, <code>run-quality-gates.py</code>) роняет сборку, когда это не так.</p><p><strong>Раскрытая способность: документы, <em>написанные</em> ИИ, выходят однородными.</strong> Когда <code>/discovery-to-srs</code> или Jira-пайплайн генерирует спеку, <code>STYLE.md</code> — это схема, под которую она пишется; так машинно-созданные документы получаются той же формы, что и написанные вручную, и следующий агент, читающий их, точно знает, куда смотреть. Гайд, который человек просто <em>читает</em>, — рекомендательный; гайд, который есть перечень плюс CI-гейт, — контракт, соблюдаемый обеими сторонами.</p><h3 id="modules-md-и-папки-жизненного-цикла-—-честность-во-времени"><a href="#modules-md-и-папки-жизненного-цикла-—-честность-во-времени" class="headerlink" title="modules.md и папки жизненного цикла — честность во времени"></a>modules.md и папки жизненного цикла — честность во времени</h3><p><code>modules.md</code> каждой фичи — это <strong>источник истины</strong> о том, какой код к ней относится, и правило такое: добавил модуль в коде — обнови <code>modules.md</code> в <em>том же PR</em>. Подпапки <code>_drafts/</code> и <code>_archive/</code>, управляемые полем <code>status</code>, не дают работе-в-процессе и мёртвым документам засорять то, что агент считает авторитетным.</p><p><strong>Раскрытая способность: агент может доверять тому, что читает.</strong> Самый дорогой режим отказа для документ-ориентированного ИИ — уверенно действовать по устаревшей спеке: ошибки не будет, он просто сделает не то. Обновления модулей в том же PR и явный статус жизненного цикла — это дешёвый, невзрачный механизм, не дающий «в документации написано X» и «код делает X» разойтись.</p><h2 id="Соединительная-ткань"><a href="#Соединительная-ткань" class="headerlink" title="Соединительная ткань"></a>Соединительная ткань</h2><p>Две половины встречаются в одном инварианте, которому следует каждый навык: <strong>разрешить, прочитать, действовать.</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Jira-тикет  →  INDEX.md (модуль → фича)</span><br><span class="line">            →  modules.md (какие модули)</span><br><span class="line">            →  &lt;Feature&gt;.srs.md (задокументированное поведение)</span><br><span class="line">            →  реализация под управлением .claude/rules/*</span><br><span class="line">            →  ревью через специализированных суб-агентов</span><br><span class="line">            →  /update-docs помечает, какие спеки устарели</span><br></pre></td></tr></table></figure><p><code>task-worker</code> проживает этот цикл от и до. Он никогда не гадает, где фича и как она должна себя вести, — он ищет и то и другое, строит против задокументированного контракта, реализует под правилами, ревьюит со специалистами, а затем проверяет, не сделали ли его же изменения документацию устаревшей. Системы из первого поста — Q&amp;A-бот Doc-Chat, пайплайн Jira→Docs — это просто <em>ещё потребители</em> той же структуры. Они работают, потому что индекс, спеки и правила уже существуют, чтобы их читать.</p><h2 id="Что-теперь-умеет-ИИ-—-коротко"><a href="#Что-теперь-умеет-ИИ-—-коротко" class="headerlink" title="Что теперь умеет ИИ — коротко"></a>Что теперь умеет ИИ — коротко</h2><table><thead><tr><th>Слой</th><th>Какую способность раскрывает</th></tr></thead><tbody><tr><td><code>.claude/rules/</code></td><td>Пишет код по <em>вашим</em> соглашениям, а не по среднему GitHub</td></tr><tr><td><code>.claude/skills/</code></td><td>Выполняет воспроизводимый процесс, а не импровизирует</td></tr><tr><td><code>.claude/agents/</code></td><td>Глубокое параллельное ревью — специалисты, а не один уставший универсал</td></tr><tr><td><code>.claude/hooks</code> + <code>scripts/</code></td><td>Тратит токены на суждения; сгружает рутину</td></tr><tr><td><code>docs/INDEX.md</code></td><td>Извлекает по индексу вместо слепого поиска</td></tr><tr><td><code>docs/llms.txt</code> + <code>STYLE.md</code></td><td>Читает нужный документ; пишет новые в единой форме</td></tr><tr><td><code>modules.md</code> + жизненный цикл</td><td>Доверяет тому, что читает — документация не гниёт молча</td></tr></tbody></table><h2 id="Почему-именно-в-репозитории"><a href="#Почему-именно-в-репозитории" class="headerlink" title="Почему именно в репозитории"></a>Почему именно <em>в репозитории</em></h2><p>Каждая выгода выше зависит от того, что этот контекст живёт в репозитории, а не в Confluence или вики. Четыре причины, и все они усиливают друг друга:</p><ul><li><strong>Автозагрузка.</strong> <code>CLAUDE.md</code> и <code>.claude/</code> подтягиваются в каждую сессию агента без всякой настройки. Вики-страницу нужно найти, скачать и вставить — а значит, обычно её не вставляют.</li><li><strong>Привязка к версии.</strong> Документация на том же коммите, что и код. Переключитесь на ветку полугодовой давности — получите спеки и правила <em>именно той</em> ветки, без гаданий «какая версия этой Confluence-страницы соответствует этому тегу?».</li><li><strong>Тот же diff.</strong> Изменение поведения и обновление его документа едут в одном PR и ревьюятся вместе. <code>/update-docs</code> существует именно чтобы держать эту связь тугой. Confluence обновляют неделями позже, если вообще.</li><li><strong>Грепается и дружит с кэшем.</strong> Простой текст на живой файловой системе означает, что агент может <code>Grep</code> по <em>текущему</em> состоянию — без индекса эмбеддингов, который надо пересинхронизировать (аргумент «без векторной БД» из первого поста). А малоизменчивый простой текст держит промпт-кэш тёплым, что на масштабе — реальная статья расходов.</li></ul><h2 id="Итог"><a href="#Итог" class="headerlink" title="Итог"></a>Итог</h2><p>Репозиторий, готовый для агента, — это не один большой умный промпт. Это две скучные слоистые половины: <code>.claude/</code>, кодирующая, <em>как работает команда</em>, и <code>docs/</code>, кодирующая, <em>что делает система</em>, — соединённые дисциплиной <em>разрешить, прочитать, действовать</em>. Каждый файл сам по себе мал и невзрачен; вместе они — разница между ИИ, который исследует вашу кодовую базу, и ИИ, который ею <em>управляет</em>.</p><p>Вложение реальное, но это то же вложение, что делает кодовую базу понятной новому senior-инженеру, — только теперь вы делаете его один раз и сразу на все будущие запуски агента. Начните с трёх файлов из первого поста (<code>CLAUDE.md</code>, несколько правил, индекс), выпустите один навык и дайте <code>/review-and-rule</code> вырастить остальное из того, что ваши ревью уже знают.</p><h3 id="Что-почитать-дальше"><a href="#Что-почитать-дальше" class="headerlink" title="Что почитать дальше"></a>Что почитать дальше</h3><ul><li><a href="/ru/2026/06/27/documentation-in-the-ai-era/">Документация в эпоху ИИ: документация — новый исходный код</a> — системы, построенные поверх этой структуры</li><li><a href="/ru/2026/06/27/documentation-driven-development/">D3: Разработка, управляемая документацией</a> — процесс, производящий спеки</li><li>Anthropic — <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code best practices</a> и <a href="https://code.claude.com/">Agent Skills</a></li><li><a href="https://llmstxt.org/">llms.txt</a> — соглашение о машинно-читаемой точке входа</content></li></ul>]]></content>
    
    
    <summary type="html">Экскурсия по настоящему Android-репозиторию из 100+ модулей, обустроенному для LLM-агента — навыки, правила и суб-агенты в `.claude/`, которые задают *как* работать, и документация в `docs/`, которая описывает *что* делает система, — и какая конкретная способность раскрывается на каждом уровне.</summary>
    
    
    
    <category term="AI" scheme="https://devapro.github.io/categories/AI/"/>
    
    
    <category term="android" scheme="https://devapro.github.io/tags/android/"/>
    
    <category term="ai" scheme="https://devapro.github.io/tags/ai/"/>
    
    <category term="documentation" scheme="https://devapro.github.io/tags/documentation/"/>
    
    <category term="llm" scheme="https://devapro.github.io/tags/llm/"/>
    
    <category term="claude" scheme="https://devapro.github.io/tags/claude/"/>
    
    <category term="agents" scheme="https://devapro.github.io/tags/agents/"/>
    
    <category term="skills" scheme="https://devapro.github.io/tags/skills/"/>
    
    <category term="developer-experience" scheme="https://devapro.github.io/tags/developer-experience/"/>
    
  </entry>
  
  <entry>
    <title>AI Skill: от черновика к проверенному SRS</title>
    <link href="https://devapro.github.io/ru/2026/05/18/anatomy-of-a-claude-skill-ru/"/>
    <id>https://devapro.github.io/ru/2026/05/18/anatomy-of-a-claude-skill-ru/</id>
    <published>2026-05-18T09:00:00.000Z</published>
    <updated>2026-06-28T15:21:53.868Z</updated>
    
    <content type="html"><![CDATA[<p>В посте <a href="/ru/2026/06/27/documentation-driven-development-ru/">D3: Разработка, управляемая документацией</a> я доказывал, что самый выгодный ход в жизни фичи — написать полную спецификацию <em>до</em> того, как появится код. В посте <a href="/ru/2026/06/27/documentation-in-the-ai-era-ru/">Документация в эпоху ИИ</a> — что документация стала новым исходным кодом, и что любой агентский скил хорош ровно настолько, насколько хороша документация, которую он читает. Оба поста опираются на одно негласное допущение: что хорошая спецификация действительно будет написана.</p><p>Именно на этом допущении команды всегда и спотыкались. Поэтому здесь я максимально приближаю объектив к одному конкретному ответу на вопрос «а как надёжно произвести эту спецификацию?» — к одному переиспользуемому <strong>скилу</strong>, который берёт черновой документ-исследование и превращает его в аккуратный SRS, опирающийся на код, а затем поручает второму агенту проверить результат, прежде чем вы на него положитесь.</p><h2 id="Сначала-—-что-такое-скил"><a href="#Сначала-—-что-такое-скил" class="headerlink" title="Сначала — что такое скил?"></a>Сначала — что такое скил?</h2><p><strong>Скил (skill)</strong> — это именованная переиспользуемая процедура, которую агент может вызвать: папка с файлом-инструкцией <code>SKILL.md</code> (и, опционально, со вспомогательными агентами, скриптами или шаблонами), в которой закодировано, <em>как</em> хорошо выполнять повторяющуюся задачу. Вместо того чтобы каждый раз заново объяснять один и тот же многошаговый процесс в новом промпте, вы записываете его один раз — и агент ему следует: те же шаги, та же планка качества, тот же формат вывода при каждом запуске.</p><p>Это разница между «иди напиши спецификацию», сказанным новичку, и чек-листом, который старший инженер отшлифовал на десятке фич. Скил — это и есть такой чек-лист, ставший исполняемым.</p><h2 id="Проблема-разрыв-между-черновиком-и-спецификацией"><a href="#Проблема-разрыв-между-черновиком-и-спецификацией" class="headerlink" title="Проблема: разрыв между черновиком и спецификацией"></a>Проблема: разрыв между черновиком и спецификацией</h2><p>Любая фича начинается с чего-то сырого — пунктов в документе, скриншота, абзаца от продакта, нескольких заметок с созвона. Превратить это в спецификацию, по которой разработчик сможет строить, — реальная работа, и она проваливается четырьмя предсказуемыми способами:</p><ul><li><strong>Остаётся расплывчатой.</strong> В черновике написано «показать пользователю его элементы». Не написано, что видит разлогиненный пользователь, что показывается, пока список грузится, что — когда список пуст, и что происходит при ошибке запроса. Именно в этих пробелах и рождаются баги.</li><li><strong>Не опирается на код.</strong> Спецификация, написанная только по черновику, выдумывает имена модулей, угадывает форму API и игнорирует паттерны, которые уже есть в репозитории. Потом разработчик тратит первый день на сверку спецификации с реальностью.</li><li><strong>Расползается формат.</strong> Каждый пишет спецификации немного по-своему, поэтому никакие два документа не читаются одинаково — а LLM, которая прочитает их позже (см. два предыдущих поста), получает разнородный корпус.</li><li><strong>Никто её не проверяет.</strong> Спецификации верят потому, что она существует, а не потому, что её сверили. Ошибки доживают до самого кода.</li></ul><p>Это и есть та дорогая, скучная работа, дешевизна которой — обязательное условие для D3. Скил — это способ сделать её дешёвой <em>и</em> надёжной: не за счёт того, что пишешь быстрее, а за счёт того, что вся дисциплина закодирована один раз.</p><h2 id="Анатомия-скила"><a href="#Анатомия-скила" class="headerlink" title="Анатомия скила"></a>Анатомия скила</h2><p>Скил — это папка из двух частей: <code>SKILL.md</code>, задающий рабочий процесс, и вспомогательный агент-рецензент, которого он запускает ближе к концу. Вот метаданные-триггер в начале <code>SKILL.md</code>:</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: discovery-to-srs</span><br><span class="line">description: Превращает черновой/исследовательский Markdown-документ в</span><br><span class="line">  аккуратную спецификацию (SRS). Срабатывает на «напиши SRS»,</span><br><span class="line">  «преврати это исследование в документацию», «задокументируй фичу»</span><br><span class="line">  или когда пользователь делится черновиком с описанием фичи.</span><br><span class="line"><span class="section">argument-hint: &quot;&lt;путь-к-discovery.md&gt;&quot;</span></span><br><span class="line"><span class="section">---</span></span><br></pre></td></tr></table></figure><p><code>description</code> — не украшение, а то, как агент решает, <em>когда</em> взяться за этот скил. Формулировка вокруг реальных слов пользователя («у меня есть черновик», «задокументируй фичу») и делает вызов автоматическим, а не тем, что надо помнить самому.</p><p>Тело <code>SKILL.md</code> — это рабочий процесс из восьми шагов. По отдельности шаги важны меньше, чем форма, которую они образуют, поэтому вот он целиком:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">1. Прочитать черновик       → извлечь скоуп, экраны, модули, что расплывчато</span><br><span class="line">2. Исследовать и закрыть    → искать в коде и докуменах ПАРАЛЛЕЛЬНО</span><br><span class="line">   пробелы</span><br><span class="line">3. Задать уточняющие        → только то, что нельзя достать из кода/докум.</span><br><span class="line">   вопросы</span><br><span class="line">4. Определить расположение   → новая фича или существующая папка</span><br><span class="line">5. Написать SRS              → следовать каноническому формату</span><br><span class="line">6. Запустить независимого рецензента</span><br><span class="line">7. Применить замечания       → доработать, при необходимости пере-проверить</span><br><span class="line">8. Показать результат        → НЕ начинать реализацию</span><br></pre></td></tr></table></figure><p>Четыре из этих шагов — там, где сосредоточена настоящая выгода.</p><h3 id="Шаг-2-—-закрыть-пробелы-по-реальному-коду"><a href="#Шаг-2-—-закрыть-пробелы-по-реальному-коду" class="headerlink" title="Шаг 2 — закрыть пробелы по реальному коду"></a>Шаг 2 — закрыть пробелы по реальному коду</h3><p>Это шаг, который отделяет настоящую спецификацию от красиво оформленной догадки. Вместо того чтобы развивать черновик в его же терминах, агент исследует <strong>код и документацию параллельно</strong>, чтобы заполнить всё, что черновик оставил расплывчатым:</p><ul><li>В документации: прочитать индекс, чтобы понять, что уже есть и как оно организовано; если у фичи уже есть папка — прочитать её спецификацию и сквозные технические заметки (навигация, авторизация, диплинки).</li><li>В коде: найти grep’ом модули, относящиеся к фиче; прочитать соответствующие файлы состояния&#x2F;логики, чтобы узнать <em>реально реализованное</em> поведение; свериться с графами навигации, где находятся экраны; прочитать интерфейсы API и модели данных; проверить флаги, управляющие раскаткой.</li></ul><p>Главная инструкция здесь вот какая: <strong>если поведение выведено из кода, а не заявлено в черновике, его всё равно правильно включить — вы же его проверили.</strong> Именно это правило превращает расплывчатый черновик в спецификацию, опирающуюся на то, что система делает на самом деле.</p><h3 id="Шаг-3-—-спрашивать-только-то-что-нельзя-вывести"><a href="#Шаг-3-—-спрашивать-только-то-что-нельзя-вывести" class="headerlink" title="Шаг 3 — спрашивать только то, что нельзя вывести"></a>Шаг 3 — спрашивать только то, что нельзя вывести</h3><p>После исследования агент определяет, что <em>всё ещё</em> неясно, и спрашивает — но скил прямо требует, чтобы это был крайний случай, чтобы вопросы шли пачкой и только по настоящим неизвестным: неоднозначный скоуп, противоречия между черновиком и кодом, неописанное состояние пользователя («что должен видеть гость?») или интерфейс, у которого нигде нет дизайн-референса. <strong>Нельзя спрашивать о том, что можно выяснить самому из кода или документации.</strong> В этом и разница между ассистентом, который уважает ваше время, и тем, кто превращает любую задачу в допрос.</p><h3 id="Шаг-5-—-закрепить-формат-за-каноническим-примером"><a href="#Шаг-5-—-закрепить-формат-за-каноническим-примером" class="headerlink" title="Шаг 5 — закрепить формат за каноническим примером"></a>Шаг 5 — закрепить формат за каноническим примером</h3><p>Скил не описывает формат вывода абстрактно — он указывает на реальную, уже существующую спецификацию как на эталон стиля и перечисляет обязательные разделы: обзор, таблицу «модуль → назначение», функциональные требования по каждому экрану с разбивкой <strong>по состояниям пользователя</strong> (гость, бесплатный, платный…), интеграцию с API и флаги. Правила стиля конкретны: повелительные формулировки («Нажатие на X открывает Y»), явная персистентность («хранится локально» против «только в рантайме»), точные имена модулей в обратных кавычках. Привязка к примеру и держит все спецификации скила одинаково читаемыми — что как раз и делает итоговый корпус понятным для следующего агента.</p><h3 id="Шаги-6–7-—-независимый-рецензент-со-своим-чек-листом"><a href="#Шаги-6–7-—-независимый-рецензент-со-своим-чек-листом" class="headerlink" title="Шаги 6–7 — независимый рецензент со своим чек-листом"></a>Шаги 6–7 — независимый рецензент со своим чек-листом</h3><p>Эту часть чаще всего пропускают самодельные промпты — а она важнее всего. Когда SRS написан, скил запускает <strong>отдельного агента-рецензента</strong> со свежим контекстом и собственными инструкциями. Это не расплывчатое «глянь-ка» — у рецензента настоящий чек-лист:</p><ul><li><strong>Полнота</strong> — покрыт ли каждый экран? Каждое состояние пользователя? Состояния загрузки, пустоты и ошибки? Каждый интерактивный элемент? Каждый флаг?</li><li><strong>Точность</strong> — <em>действительно ли существуют</em> в кодовой базе указанные имена модулей, эндпоинты API и имена флагов? Имя, которое никуда не ведёт, — это критическая проблема, а не придирка.</li><li><strong>Согласованность</strong> — не противоречит ли это какой-нибудь уже задокументированной фиче?</li><li><strong>Формат и открытые вопросы</strong> — соблюдена ли принятая структура и что разработчику всё ещё придётся спросить перед сборкой?</li></ul><p>Рецензент завершает одним из трёх вердиктов — <strong>APPROVED</strong>, <strong>APPROVED WITH NOTES</strong> или <strong>NEEDS REVISION</strong> — и процесс ветвится по нему: одобрить и закончить, применить замечания и закончить либо доработать и запустить рецензента <em>снова</em>. Его финальную инструкцию стоит процитировать: <em>«Будь прямым. Не раздувай находки ради видимости тщательности и не смягчай критические проблемы ради вежливости. Цель — пригодный документ, а не идеальная оценка»</em>.</p><p>Два агента, два контекста, две задачи: один пишет, полностью зная черновик и код, другой проверяет свежим взглядом по чек-листу. Именно это разделение и ловит слепые зоны автора — по той же причине, по которой мы не даём людям мёржить собственные PR без ревью.</p><h2 id="Почему-скил-а-не-просто-хороший-промпт"><a href="#Почему-скил-а-не-просто-хороший-промпт" class="headerlink" title="Почему скил, а не просто хороший промпт"></a>Почему скил, а не просто хороший промпт</h2><p>Всё это можно один раз вставить в чат и получить приличный SRS. Причина сделать из этого скил — во всём, что происходит <em>во второй</em> раз:</p><ul><li><strong>Согласованность.</strong> Каждая спецификация выходит в одной и той же форме, поэтому корпус остаётся читаемым — и для людей, и для агентов, которые прочитают его позже.</li><li><strong>Планка качества путешествует.</strong> Правило закрытия пробелов, правило «не спрашивай то, что можно вывести», чек-лист рецензента — это выстраданные уроки. Скил фиксирует их один раз, чтобы они применялись при каждом запуске, а не жили в чьей-то голове.</li><li><strong>Это контрольная точка, а не чёрный ящик.</strong> Процесс намеренно заканчивается <em>до</em> реализации: SRS — это и есть результат, проверенный и согласованный, ровно тот артефакт, который D3 хочет зафиксировать до старта кода.</li><li><strong>Он компонуется.</strong> Этот скил производит спецификацию; другие скилы (спланировать фичу, реализовать её, отревьюить PR) её потребляют. Весь конвейер D3 — это скилы, передающие друг другу типизированные артефакты.</li></ul><p>Промпт — это разовая вещь. Скил — это процесс, которому можно доверить отработать одинаково и в пятницу в 17:00, и в тот первый раз, когда вы его записали.</p><h2 id="Об-инструментах"><a href="#Об-инструментах" class="headerlink" title="Об инструментах"></a>Об инструментах</h2><p>Я описываю это на примере Claude и его формата скилов, потому что на нём это и работает, но сама <em>идея</em> переносима. Скил — это всего лишь записанная процедура плюс опциональный агент-рецензент; любая агентская среда, которая умеет читать файлы, искать по кодовой базе и запускать второй проход, способна сделать то же самое. Выгода не в вендоре, а в трёх вещах, которые доступны любой способной модели: рабочий процесс, достаточно конкретный, чтобы следовать ему по шагам; дисциплина сверять с реальным кодом, а не угадывать; и независимый рецензент, чтобы ничего не уехало непроверенным.</p><h2 id="Итог"><a href="#Итог" class="headerlink" title="Итог"></a>Итог</h2><p>D3 говорит: пиши спецификацию до кода. «Документация — новый исходный код» говорит: эту спецификацию и будет читать каждый последующий агент. Этот скил — маленькая конкретная машина, которая делает производство такой спецификации дешёвым и заслуживающим доверия: прочитать черновик, закрыть его пробелы по реальному коду, спросить только то, что нельзя вывести, написать в единой форме и дать второму агенту разобрать её по косточкам, прежде чем на неё кто-то положится. Закодируйте эту дисциплину один раз — и сырой черновик станет проверенным справочником. Каждый раз, а не только когда кто-то вспомнит, что надо быть внимательным.<br></content></p>]]></content>
    
    
    <summary type="html">Подробный разбор одного переиспользуемого ИИ-скила, который превращает черновое описание фичи в полную спецификацию, опирающуюся на реальный код, — а затем поручает независимому агенту проверить её, прежде чем вы ей поверите.</summary>
    
    
    
    <category term="AI" scheme="https://devapro.github.io/categories/AI/"/>
    
    
    <category term="ai" scheme="https://devapro.github.io/tags/ai/"/>
    
    <category term="documentation" scheme="https://devapro.github.io/tags/documentation/"/>
    
    <category term="llm" scheme="https://devapro.github.io/tags/llm/"/>
    
    <category term="claude" scheme="https://devapro.github.io/tags/claude/"/>
    
    <category term="agents" scheme="https://devapro.github.io/tags/agents/"/>
    
    <category term="skills" scheme="https://devapro.github.io/tags/skills/"/>
    
    <category term="srs" scheme="https://devapro.github.io/tags/srs/"/>
    
    <category term="developer-experience" scheme="https://devapro.github.io/tags/developer-experience/"/>
    
  </entry>
  
  <entry>
    <title>AI Skill: From Discovery Notes to a Reviewed SRS</title>
    <link href="https://devapro.github.io/en/2026/05/18/anatomy-of-a-claude-skill/"/>
    <id>https://devapro.github.io/en/2026/05/18/anatomy-of-a-claude-skill/</id>
    <published>2026-05-18T09:00:00.000Z</published>
    <updated>2026-06-28T15:21:53.868Z</updated>
    
    <content type="html"><![CDATA[<p>In <a href="/en/2026/06/27/documentation-driven-development/">D3: Documentation Driven Development</a> I argued that the highest-leverage move in a feature’s life is to write a complete spec <em>before</em> any code exists. In <a href="/en/2026/06/27/documentation-in-the-ai-era/">Documentation in the AI Era</a> I argued that docs are the new source code, and that every agent skill is only as good as the docs it reads. Both posts lean on the same quiet assumption: that a good specification actually gets written.</p><p>That assumption is exactly where teams have always failed. So this post zooms all the way in on a single, concrete answer to “but how do you reliably produce that spec?” — one reusable <strong>skill</strong> that takes a rough discovery document and turns it into a polished, code-grounded SRS, then has a second agent review it before you trust the result.</p><h2 id="First-what-is-a-skill"><a href="#First-what-is-a-skill" class="headerlink" title="First, what is a skill?"></a>First, what is a skill?</h2><p>A <strong>skill</strong> is a named, reusable procedure an agent can invoke — a folder containing a <code>SKILL.md</code> instruction file (and optionally helper agents, scripts, or templates) that encodes <em>how</em> to do a recurring job well. Instead of re-explaining the same multi-step process in a fresh prompt every time, you write it down once and the agent follows it: same steps, same quality bar, same output format, every run.</p><p>Think of it as the difference between telling a new hire “go write a spec” and handing them a checklist that a senior engineer refined over a dozen features. The skill is that checklist, made executable.</p><h2 id="The-Problem-the-discovery-to-spec-gap"><a href="#The-Problem-the-discovery-to-spec-gap" class="headerlink" title="The Problem: the discovery-to-spec gap"></a>The Problem: the discovery-to-spec gap</h2><p>Every feature starts as something rough — bullet points in a doc, a screenshot, a paragraph from product, a few notes from a discovery call. Turning that into a specification a developer can build from is real work, and it fails in four predictable ways:</p><ul><li><strong>It stays vague.</strong> The draft says “show the user their items.” It doesn’t say what a logged-out user sees, what shows while the list loads, what shows when the list is empty, or what happens when the API call fails. Those gaps are exactly where bugs are born.</li><li><strong>It isn’t grounded in the code.</strong> A spec written from the draft alone invents module names, guesses at API shapes, and ignores patterns that already exist in the repo. The developer then spends the first day reconciling the spec with reality.</li><li><strong>The format drifts.</strong> Everyone writes specs a little differently, so no two documents are navigable the same way — and an LLM reading them later (see the previous two posts) gets an inconsistent corpus.</li><li><strong>Nobody checks it.</strong> The spec is trusted because it exists, not because it was verified. Mistakes survive all the way into code.</li></ul><p>This is the expensive, boring work D3 depends on being cheap. A skill is how you make it cheap <em>and</em> reliable — not by writing faster, but by encoding the whole discipline once.</p><h2 id="Anatomy-of-the-skill"><a href="#Anatomy-of-the-skill" class="headerlink" title="Anatomy of the skill"></a>Anatomy of the skill</h2><p>The skill is a folder with two parts: a <code>SKILL.md</code> that defines the workflow, and a helper reviewer agent it spawns near the end. Here’s the trigger metadata at the top of <code>SKILL.md</code>:</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: discovery-to-srs</span><br><span class="line">description: Converts a Markdown discovery/draft document into a polished</span><br><span class="line">  Software Requirements Specification (SRS). Triggers on &quot;write an SRS&quot;,</span><br><span class="line">  &quot;turn this discovery into docs&quot;, &quot;document this feature&quot;, or when the</span><br><span class="line">  user shares a draft describing a feature they want documented.</span><br><span class="line"><span class="section">argument-hint: &quot;<span class="language-xml"><span class="tag">&lt;<span class="name">path-to-discovery.md</span>&gt;</span></span>&quot;</span></span><br><span class="line"><span class="section">---</span></span><br></pre></td></tr></table></figure><p>The <code>description</code> isn’t decoration — it’s how the agent decides <em>when</em> to reach for this skill. Phrasing it around the user’s actual words (“I have a draft”, “document this feature”) is what makes invocation feel automatic rather than something you have to remember.</p><p>The body of <code>SKILL.md</code> is an eight-step workflow. The steps matter less individually than the shape they form, so here it is end to end:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">1. Read the discovery doc      → extract scope, screens, modules, what&#x27;s vague</span><br><span class="line">2. Research &amp; fill the gaps    → search code + docs IN PARALLEL</span><br><span class="line">3. Ask clarifying questions    → only what code/docs can&#x27;t answer</span><br><span class="line">4. Determine the location      → new vs. existing feature folder</span><br><span class="line">5. Write the SRS               → follow a canonical format</span><br><span class="line">6. Spawn an independent reviewer</span><br><span class="line">7. Apply review feedback       → revise, re-review if needed</span><br><span class="line">8. Present the result          → do NOT start implementation</span><br></pre></td></tr></table></figure><p>Four of those steps are where the real leverage lives.</p><h3 id="Step-2-—-Fill-the-gaps-from-the-actual-code"><a href="#Step-2-—-Fill-the-gaps-from-the-actual-code" class="headerlink" title="Step 2 — Fill the gaps from the actual code"></a>Step 2 — Fill the gaps from the actual code</h3><p>This is the step that separates a real spec from a polished-looking guess. Rather than expanding the draft on its own terms, the agent explores <strong>the code and the docs in parallel</strong> to fill in everything the draft left vague:</p><ul><li>In the docs: read the docs index to learn what already exists and how it’s organized; if the feature already has a folder, read its existing spec and any cross-cutting technical notes (navigation, auth, deep links).</li><li>In the code: grep for modules related to the feature; read the relevant state&#x2F;logic files to learn the <em>actually implemented</em> behavior; check navigation graphs for where screens fit; read API interfaces and data models; check any feature flags gating rollout.</li></ul><p>The instruction the skill gives here is the important one: <strong>if a behavior is inferred from code rather than stated in the draft, it’s still correct to include — you just verified it.</strong> That single rule is what turns a vague draft into a spec grounded in what the system actually does.</p><h3 id="Step-3-—-Ask-only-what-you-can’t-infer"><a href="#Step-3-—-Ask-only-what-you-can’t-infer" class="headerlink" title="Step 3 — Ask only what you can’t infer"></a>Step 3 — Ask only what you can’t infer</h3><p>After research, the agent identifies what’s <em>still</em> unclear and asks — but the skill is explicit that this is a last resort, batched, and limited to genuine unknowns: ambiguous scope, conflicting signals between draft and code, an undescribed user state (“what should a guest see?”), or UI with no design reference anywhere. <strong>It must not ask about anything it could answer itself from code or docs.</strong> This is the difference between an assistant that respects your time and one that turns every task into an interview.</p><h3 id="Step-5-—-Anchor-the-format-to-a-canonical-example"><a href="#Step-5-—-Anchor-the-format-to-a-canonical-example" class="headerlink" title="Step 5 — Anchor the format to a canonical example"></a>Step 5 — Anchor the format to a canonical example</h3><p>The skill doesn’t describe the output format in the abstract; it points at a real, existing spec as the style reference and lists the required sections — overview, module-to-purpose table, per-screen functional requirements broken down <strong>by user state</strong> (guest, free, paid…), API integration, and feature flags. The style rules are concrete: imperative phrasing (“Tapping X opens Y”), explicit persistence (“stored locally” vs. “runtime only”), exact module names in backticks. Anchoring to an example is what keeps every spec the skill produces navigable the same way — which is precisely what makes the resulting corpus readable by the next agent.</p><h3 id="Steps-6–7-—-An-independent-reviewer-with-its-own-rubric"><a href="#Steps-6–7-—-An-independent-reviewer-with-its-own-rubric" class="headerlink" title="Steps 6–7 — An independent reviewer, with its own rubric"></a>Steps 6–7 — An independent reviewer, with its own rubric</h3><p>This is the part most home-grown prompts skip, and it’s the most important. Once the SRS is written, the skill spawns a <strong>separate reviewer agent</strong> with a fresh context and its own instructions. It isn’t a vague “check this over” — the reviewer has a real rubric:</p><ul><li><strong>Completeness</strong> — is every screen covered? Every user state? Loading, empty, and error states? Every interactive element? Every flag?</li><li><strong>Accuracy</strong> — do the module names, API endpoints, and flag names <em>actually exist</em> in the codebase? A name that doesn’t resolve is a critical issue, not a nit.</li><li><strong>Consistency</strong> — does it contradict any already-documented feature?</li><li><strong>Format &amp; open questions</strong> — does it follow the house structure, and what would a developer still need to ask before building?</li></ul><p>The reviewer ends with one of three verdicts — <strong>APPROVED</strong>, <strong>APPROVED WITH NOTES</strong>, or <strong>NEEDS REVISION</strong> — and the workflow branches on it: approve and finish, apply notes and finish, or revise and run the reviewer <em>again</em>. Its closing instruction is worth quoting: <em>“Be direct. Don’t inflate findings to seem thorough, and don’t soften critical issues to seem nice. The goal is a usable document, not a perfect score.”</em></p><p>Two agents, two contexts, two jobs: one writes with full knowledge of the draft and code, the other audits with fresh eyes against a checklist. That separation is what catches the writer’s blind spots — the same reason we don’t let people merge their own PRs unreviewed.</p><h2 id="Why-a-skill-and-not-just-a-good-prompt"><a href="#Why-a-skill-and-not-just-a-good-prompt" class="headerlink" title="Why a skill, and not just a good prompt"></a>Why a skill, and not just a good prompt</h2><p>You could paste all of this into a chat once and get a decent SRS. The reason to make it a skill is everything that happens the <em>second</em> time:</p><ul><li><strong>Consistency.</strong> Every spec comes out in the same shape, so the corpus stays navigable — for humans and for the agents that read it later.</li><li><strong>The quality bar travels.</strong> The gap-filling rule, the “don’t ask what you can infer” rule, the reviewer’s rubric — those are hard-won lessons. A skill captures them once so they apply on every run instead of living in one person’s head.</li><li><strong>It’s a checkpoint, not a black box.</strong> The workflow deliberately ends <em>before</em> implementation: the SRS is the deliverable, reviewed and signed off, exactly the artifact D3 wants locked before code begins.</li><li><strong>It composes.</strong> This skill produces the spec; other skills (plan a feature, implement it, review the PR) consume it. The whole D3 pipeline is skills handing typed artifacts to one another.</li></ul><p>A prompt is a one-off. A skill is a process you can trust to run the same way at 5pm on a Friday as it did the first time you wrote it down.</p><h2 id="A-note-on-tooling"><a href="#A-note-on-tooling" class="headerlink" title="A note on tooling"></a>A note on tooling</h2><p>I describe this with Claude and its skills format because that’s what it runs on, but the <em>idea</em> is portable. A skill is just a written-down procedure plus an optional reviewer agent — any agent runtime that can read files, search a codebase, and spawn a second pass can do the same thing. The leverage isn’t in the vendor; it’s in three things any capable model can use: a workflow specific enough to follow step by step, the discipline to verify against real code instead of guessing, and an independent reviewer so nothing ships unchecked.</p><h2 id="Bottom-Line"><a href="#Bottom-Line" class="headerlink" title="Bottom Line"></a>Bottom Line</h2><p>D3 says write the spec before the code. Docs-as-source-code says that spec is what every later agent will read. This skill is the small, concrete machine that makes producing that spec cheap and trustworthy: read the draft, fill its gaps from the real code, ask only what you can’t infer, write it in a consistent shape, and have a second agent tear it apart before anyone relies on it. Encode that discipline once, and a rough draft becomes a reviewed reference — every time, not just when someone remembers to be careful.<br></content><br></invoke></p>]]></content>
    
    
    <summary type="html">A close look at one reusable AI skill that turns a rough feature draft into a complete, code-grounded SRS — and then has an independent agent review it before you trust it.</summary>
    
    
    
    <category term="AI" scheme="https://devapro.github.io/categories/AI/"/>
    
    
    <category term="ai" scheme="https://devapro.github.io/tags/ai/"/>
    
    <category term="documentation" scheme="https://devapro.github.io/tags/documentation/"/>
    
    <category term="llm" scheme="https://devapro.github.io/tags/llm/"/>
    
    <category term="claude" scheme="https://devapro.github.io/tags/claude/"/>
    
    <category term="agents" scheme="https://devapro.github.io/tags/agents/"/>
    
    <category term="skills" scheme="https://devapro.github.io/tags/skills/"/>
    
    <category term="srs" scheme="https://devapro.github.io/tags/srs/"/>
    
    <category term="developer-experience" scheme="https://devapro.github.io/tags/developer-experience/"/>
    
  </entry>
  
  <entry>
    <title>Как освободить место на переполненном сервере Linux</title>
    <link href="https://devapro.github.io/ru/2026/05/07/investigation-ru/"/>
    <id>https://devapro.github.io/ru/2026/05/07/investigation-ru/</id>
    <published>2026-05-07T22:05:29.000Z</published>
    <updated>2026-06-28T15:21:53.869Z</updated>
    
    <content type="html"><![CDATA[<p>Если на вашем сервере Linux закончилось место, вот быстрый и эффективный способ найти и устранить проблему.</p><h2 id="1-Проверка-использования-диска"><a href="#1-Проверка-использования-диска" class="headerlink" title="1. Проверка использования диска"></a>1. Проверка использования диска</h2><p>Сначала проверьте, какая файловая система переполнена:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">df</span> -h</span><br></pre></td></tr></table></figure><p>Если подозреваете, что закончились inodes (много мелких файлов):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">df</span> -i</span><br></pre></td></tr></table></figure><h2 id="2-Поиск-самых-больших-директорий"><a href="#2-Поиск-самых-больших-директорий" class="headerlink" title="2. Поиск самых больших директорий"></a>2. Поиск самых больших директорий</h2><p>Определите, какие папки занимают больше всего места:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">du</span> -h -d 1 -x / 2&gt;/dev/null | <span class="built_in">sort</span> -hr | <span class="built_in">head</span> -20</span><br></pre></td></tr></table></figure><ul><li><code>-x</code> — сканирует только основную файловую систему.</li><li>Для интерактивного просмотра используйте <code>ncdu</code>:</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install ncdu</span><br><span class="line"><span class="built_in">sudo</span> ncdu -x /</span><br></pre></td></tr></table></figure><h2 id="3-Удаление-ненужных-больших-файлов"><a href="#3-Удаление-ненужных-больших-файлов" class="headerlink" title="3. Удаление ненужных больших файлов"></a>3. Удаление ненужных больших файлов</h2><p>Ищите необычно большие файлы, особенно в <code>/usr/local</code>, <code>/var</code> или <code>/home</code>. Часто это статические библиотеки (<code>.a</code>) или старые артефакты сборки. Если нашли ненужное — удаляйте:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">rm</span> /путь/к/большому/файлу</span><br><span class="line"><span class="built_in">sudo</span> ldconfig</span><br></pre></td></tr></table></figure><h2 id="4-Исправление-«фантомного»-использования-диска"><a href="#4-Исправление-«фантомного»-использования-диска" class="headerlink" title="4. Исправление «фантомного» использования диска"></a>4. Исправление «фантомного» использования диска</h2><p>Если <code>df</code> показывает больше занятого места, чем <code>du</code>, возможно, есть удалённые, но всё ещё открытые процессами файлы. Найдите их так:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> lsof +L1 2&gt;/dev/null | awk <span class="string">&#x27;NR==1 || $7+0 &gt; 10000000&#x27;</span></span><br></pre></td></tr></table></figure><p>Перезапустите процесс, который держит файл (например, Docker):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart docker</span><br></pre></td></tr></table></figure><h2 id="5-Профилактика-ротация-логов-Docker"><a href="#5-Профилактика-ротация-логов-Docker" class="headerlink" title="5. Профилактика (ротация логов Docker)"></a>5. Профилактика (ротация логов Docker)</h2><p>Логи Docker могут незаметно заполнить диск. Настройте ротацию логов в <code>/etc/docker/daemon.json</code>:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;log-driver&quot;</span><span class="punctuation">:</span> <span class="string">&quot;json-file&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;log-opts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;max-size&quot;</span><span class="punctuation">:</span> <span class="string">&quot;50m&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;max-file&quot;</span><span class="punctuation">:</span> <span class="string">&quot;3&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>Перезапустите Docker:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart docker</span><br></pre></td></tr></table></figure><p>Пересоздайте контейнеры, чтобы применить новую политику.</p><h2 id="Быстрые-команды-для-очистки"><a href="#Быстрые-команды-для-очистки" class="headerlink" title="Быстрые команды для очистки"></a>Быстрые команды для очистки</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">docker system <span class="built_in">df</span></span><br><span class="line">docker container prune -f</span><br><span class="line">docker builder prune -af</span><br><span class="line">docker image prune -af</span><br><span class="line">journalctl --disk-usage</span><br><span class="line"><span class="built_in">sudo</span> journalctl --vacuum-size=200M</span><br><span class="line"><span class="built_in">sudo</span> apt autoremove --purge</span><br><span class="line"><span class="built_in">sudo</span> apt clean</span><br></pre></td></tr></table></figure><hr><p>Этот вариант убирает личные истории и фокусируется на практических шагах, чтобы использовать статью как универсальное руководство.</p>]]></content>
    
    
    <summary type="html">Быстрый и эффективный способ найти и освободить место на переполненном сервере Linux</summary>
    
    
    
    <category term="Linux" scheme="https://devapro.github.io/categories/Linux/"/>
    
    
    <category term="linux" scheme="https://devapro.github.io/tags/linux/"/>
    
  </entry>
  
  <entry>
    <title>How to Recover Disk Space on a Full Linux Server</title>
    <link href="https://devapro.github.io/en/2026/05/07/investigation/"/>
    <id>https://devapro.github.io/en/2026/05/07/investigation/</id>
    <published>2026-05-07T22:05:29.000Z</published>
    <updated>2026-06-28T15:21:53.869Z</updated>
    
    <content type="html"><![CDATA[<p>If your Linux server runs out of disk space, here’s a quick, effective process to find and fix the problem.</p><h2 id="1-Check-Disk-Usage"><a href="#1-Check-Disk-Usage" class="headerlink" title="1. Check Disk Usage"></a>1. Check Disk Usage</h2><p>Start by seeing which filesystem is full:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">df</span> -h</span><br></pre></td></tr></table></figure><p>If you suspect inode exhaustion (lots of tiny files), check:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">df</span> -i</span><br></pre></td></tr></table></figure><h2 id="2-Find-Large-Directories"><a href="#2-Find-Large-Directories" class="headerlink" title="2. Find Large Directories"></a>2. Find Large Directories</h2><p>Identify which folders use the most space:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">du</span> -h -d 1 -x / 2&gt;/dev/null | <span class="built_in">sort</span> -hr | <span class="built_in">head</span> -20</span><br></pre></td></tr></table></figure><ul><li><code>-x</code> keeps the scan on the main filesystem.</li><li>For an interactive view, try <code>ncdu</code>:</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install ncdu</span><br><span class="line"><span class="built_in">sudo</span> ncdu -x /</span><br></pre></td></tr></table></figure><h2 id="3-Remove-Unneeded-Large-Files"><a href="#3-Remove-Unneeded-Large-Files" class="headerlink" title="3. Remove Unneeded Large Files"></a>3. Remove Unneeded Large Files</h2><p>Look for unusually large files, especially in <code>/usr/local</code>, <code>/var</code>, or <code>/home</code>. Static libraries (<code>.a</code> files) and old build artifacts are common culprits. If you find something you no longer need, remove it:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">rm</span> /path/to/large/file</span><br><span class="line"><span class="built_in">sudo</span> ldconfig</span><br></pre></td></tr></table></figure><h2 id="4-Fix-“Phantom”-Disk-Usage"><a href="#4-Fix-“Phantom”-Disk-Usage" class="headerlink" title="4. Fix “Phantom” Disk Usage"></a>4. Fix “Phantom” Disk Usage</h2><p>If <code>df</code> shows more used space than <code>du</code>, you may have deleted files still held open by running processes. Find them with:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> lsof +L1 2&gt;/dev/null | awk <span class="string">&#x27;NR==1 || $7+0 &gt; 10000000&#x27;</span></span><br></pre></td></tr></table></figure><p>Restart the process holding the file (e.g., Docker):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart docker</span><br></pre></td></tr></table></figure><h2 id="5-Prevent-Future-Issues-Docker-Log-Rotation"><a href="#5-Prevent-Future-Issues-Docker-Log-Rotation" class="headerlink" title="5. Prevent Future Issues (Docker Log Rotation)"></a>5. Prevent Future Issues (Docker Log Rotation)</h2><p>Docker logs can silently fill your disk. Set up log rotation in <code>/etc/docker/daemon.json</code>:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;log-driver&quot;</span><span class="punctuation">:</span> <span class="string">&quot;json-file&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;log-opts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;max-size&quot;</span><span class="punctuation">:</span> <span class="string">&quot;50m&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;max-file&quot;</span><span class="punctuation">:</span> <span class="string">&quot;3&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>Restart Docker:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart docker</span><br></pre></td></tr></table></figure><p>Recreate containers to apply the new policy.</p><h2 id="Quick-Cleanup-Commands"><a href="#Quick-Cleanup-Commands" class="headerlink" title="Quick Cleanup Commands"></a>Quick Cleanup Commands</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">docker system <span class="built_in">df</span></span><br><span class="line">docker container prune -f</span><br><span class="line">docker builder prune -af</span><br><span class="line">docker image prune -af</span><br><span class="line">journalctl --disk-usage</span><br><span class="line"><span class="built_in">sudo</span> journalctl --vacuum-size=200M</span><br><span class="line"><span class="built_in">sudo</span> apt autoremove --purge</span><br><span class="line"><span class="built_in">sudo</span> apt clean</span><br></pre></td></tr></table></figure><hr><p>Total recovered: ~7GB. Total time: about 20 minutes. Lesson value: priceless.</p>]]></content>
    
    
    <summary type="html">A quick, effective process to find and free up disk space on a full Linux server</summary>
    
    
    
    <category term="Linux" scheme="https://devapro.github.io/categories/Linux/"/>
    
    
    <category term="linux" scheme="https://devapro.github.io/tags/linux/"/>
    
  </entry>
  
  <entry>
    <title>D3: Documentation Driven Development</title>
    <link href="https://devapro.github.io/en/2026/04/27/documentation-driven-development/"/>
    <id>https://devapro.github.io/en/2026/04/27/documentation-driven-development/</id>
    <published>2026-04-27T18:00:00.000Z</published>
    <updated>2026-06-28T15:21:53.869Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>“If a feature is not documented, it doesn’t exist. If a feature is documented incorrectly, then it’s broken.”</p></blockquote><p>Most teams write specs the way they write tests for legacy code: after the fact, to make a tool happy. The real design decisions happen in Slack threads, in a Figma comment, in someone’s head during a stand-up. By the time anything is written down, the code already exists and the document is just a description of what was built — not a plan for what to build.</p><p>“Spec before code” is not a new idea. README-Driven Development (RDD — coined by Tom Preston-Werner in 2010) and documentation-first workflows have been around for well over a decade, and they always ran into the same wall: writing a complete, accurate spec by hand is expensive and boring, so teams skipped it. The discipline was right; the economics were wrong.</p><p>What changed is that an LLM agent will now do most of that expensive work — read the codebase, find the gaps, interview the team, draft the spec, and then build from it. <strong>Documentation Driven Development (D3)</strong> is that old discipline made cheap: the team produces a complete specification <em>before</em> writing code, with AI woven into every phase — not just the coding at the end. This post walks through the full flow on a feature we actually shipped this way.</p><p>If you’ve read my earlier post on <a href="/en/2026/06/27/documentation-in-the-ai-era/">Documentation in the AI Era</a>, think of D3 as the <em>process</em> counterpart to that idea: if docs are the new source code, then D3 is the development lifecycle built around them.</p><h2 id="The-Problem-We’re-Solving"><a href="#The-Problem-We’re-Solving" class="headerlink" title="The Problem We’re Solving"></a>The Problem We’re Solving</h2><p>You already know the symptoms: specs scattered across Slack, Figma, and people’s memory; QA pulled in only after the code is written; AI reached for during coding but never during planning. I won’t belabor them.</p><p>The one point worth keeping is the economics. A wrong assumption caught in a spec costs a sentence; caught in code review it costs a branch; caught in production it costs an incident. The fix gets dramatically more expensive the longer the misunderstanding survives — so the highest-leverage move is to push the hard thinking as early as possible. The reason teams didn’t was that early, thorough specs used to be too costly to produce. D3’s bet is that AI has changed that math.</p><h2 id="What-is-D3"><a href="#What-is-D3" class="headerlink" title="What is D3?"></a>What is D3?</h2><p>D3 is a process where the team builds a complete specification of a feature before writing code. What that buys each side:</p><table><thead><tr><th>For Management</th><th>For Developers</th></tr></thead><tbody><tr><td>Predictable delivery — scope is locked before coding</td><td>You receive a <strong>Final SRS</strong> before touching code</td></tr><tr><td>Fewer rework cycles — QA plans tests upfront</td><td>Claude has full context and generates plans and code from the spec</td></tr><tr><td>Clear handoff points between Product, Engineering, QA</td><td>A plan-review loop catches design issues before implementation</td></tr><tr><td>AI amplifies output at every phase, not just coding</td><td>No more “what did Product actually mean here?”</td></tr></tbody></table><p>The key shift is that the specification is not paperwork produced alongside the work — it <em>is</em> the work, until the moment code generation begins.</p><h2 id="The-End-to-End-Flow"><a href="#The-End-to-End-Flow" class="headerlink" title="The End-to-End Flow"></a>The End-to-End Flow</h2><p>Every arrow below is a checkpoint, and feedback can loop back at any stage.</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">Product (idea → PRD → prototype)</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">Collecting Requirements  ←──────── Feedback</span><br><span class="line">  [Analyst/Technical Project Manager + Other Teams]</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">Draft SRS ── Review (all stakeholders + QA)</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">Final SRS (approved)  ───→  QA → Test Plan</span><br><span class="line">        │                   (dev can start in parallel)</span><br><span class="line">        ▼</span><br><span class="line">AI-generated implementation specs (SRS × 3)</span><br><span class="line">  UI SRS · Server SRS · Client SRS</span><br><span class="line">  [AI plan ↔ AI review → Dev review]</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">AI Code ↔ AI Review → Dev Review</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">QA / VQA → PROD → Feedback</span><br></pre></td></tr></table></figure><p>Let’s walk through the phases.</p><h2 id="Phase-1-—-Idea-Initiation"><a href="#Phase-1-—-Idea-Initiation" class="headerlink" title="Phase 1 — Idea &amp; Initiation"></a>Phase 1 — Idea &amp; Initiation</h2><p><strong>Owner: Product.</strong> This phase is unchanged from how most teams already work. Product takes a business need, user request, or strategic goal and turns it into an <strong>idea → PRD → prototype</strong>. A lightweight PRD and&#x2F;or prototype is enough; it feeds directly into requirements collection. What changes in D3 is everything that comes <em>after</em> this phase.</p><h2 id="Phase-2-—-Collecting-Requirements"><a href="#Phase-2-—-Collecting-Requirements" class="headerlink" title="Phase 2 — Collecting Requirements"></a>Phase 2 — Collecting Requirements</h2><p><strong>Owner: Analyst&#x2F;Technical Project Manager. Time: ~20 minutes, AI-assisted.</strong></p><p>This is where AI first earns its keep. The process is three steps:</p><ol><li><strong>Consolidate sources</strong> into one <code>.md</code> file — PRD, designs, existing docs, API contracts.</li><li><strong>Claude finds the gaps</strong> — it checks the docs against the <em>existing codebase</em>, surfaces unknowns, and lists open questions.</li><li><strong>Claude interviews the team</strong> — it asks targeted questions to fill those gaps, then produces a clean draft.</li></ol><p>The important detail: Claude is not guessing from the PRD alone. With code&#x2F;docs exploration and the Figma MCP, it reads the actual codebase to validate requirements against what is already implemented. It knows which modules exist, which patterns to reuse, and where the new feature plugs in.</p><p><img src="/images/d3/collecting-requirements.png" alt="Claude interactively collecting requirements — asking about analytics, validating specs, and tracking open questions"></p><p>In the screenshot above the requirements collector found four open questions from codebase exploration, and rather than inventing answers it asks the team — for example, whether the new drawer should reuse the existing analytics events or define custom ones.</p><h2 id="Phase-3-—-Review-Final-SRS-Sign-off"><a href="#Phase-3-—-Review-Final-SRS-Sign-off" class="headerlink" title="Phase 3 — Review &amp; Final SRS Sign-off"></a>Phase 3 — Review &amp; Final SRS Sign-off</h2><p><strong>Owner: Analyst&#x2F;Technical Project Manager + QA + stakeholders.</strong></p><p>The draft SRS from Phase 2 is reviewed collectively until everyone signs off. The output is a single <strong>Final SRS</strong> — one source of truth, approved by all stakeholders. The moment it’s approved, two things happen at once:</p><ul><li><strong>QA writes the test plan</strong> straight from the Final SRS. Testing is designed <em>before</em> implementation begins, so bugs are caught in the spec, not in the code.</li><li><strong>Development can start in parallel.</strong> The Final SRS is enough to begin; the detailed per-domain specs (next phase) don’t block the kickoff.</li></ul><p>A good Final SRS contains:</p><ul><li>Feature requirements and acceptance criteria</li><li>Edge cases and error handling</li><li>API contracts and data flows</li><li>Fallback chains and dependencies</li></ul><h3 id="What-the-Final-SRS-Looks-Like"><a href="#What-the-Final-SRS-Looks-Like" class="headerlink" title="What the Final SRS Looks Like"></a>What the Final SRS Looks Like</h3><p>Abstract process descriptions are easy to nod along to, so here is a concrete one (sanitized into a generic feature). Say we’re adding a <strong>partner drawer behind a promo banner</strong>.</p><blockquote><p>In module <code>feature-promo-banners</code>, implement a drawer with a list of partners from the API. Reuse the drawer implementation from <code>feature-item-details</code>. The drawer opens only via deep link.</p></blockquote><p><strong>Fallback chain:</strong></p><ol><li><strong>Primary:</strong> Run the promo action (the main partner flow)</li><li><strong>Fallback 1:</strong> Open the Quick Access Drawer</li><li><strong>Fallback 2:</strong> Navigate to the “All Partners” screen</li></ol><p><strong>Acceptance criteria:</strong></p><ul><li>If the primary promo API fails (hardcoded config used), keep showing the skeleton and call the Drawer API.</li><li>If the Drawer API succeeds → show the banner; tapping it opens the Quick Access Drawer.</li><li>If the Drawer API also fails → show the banner; tapping it navigates to <code>/partners</code>.</li></ul><p>This is the level of detail the team agrees on before the LLM generates any implementation spec or code. There is no ambiguity left for the developer — or the model — to guess at.</p><h2 id="Phase-4-—-AI-Generated-Implementation-Specs-SRS-×-3"><a href="#Phase-4-—-AI-Generated-Implementation-Specs-SRS-×-3" class="headerlink" title="Phase 4 — AI-Generated Implementation Specs (SRS × 3)"></a>Phase 4 — AI-Generated Implementation Specs (SRS × 3)</h2><p><strong>Owner: Developer + Claude.</strong></p><p>The Final SRS says <em>what</em> to build; it doesn’t yet spell out <em>how</em>. That’s the next step, and it’s where the LLM takes over. With a <code>plan-feature</code> skill — plus code&#x2F;docs exploration and the Figma MCP — Claude expands the Final SRS into three detailed, domain-specific implementation specs:</p><table><thead><tr><th>Document</th><th>Owner</th><th>Focus</th></tr></thead><tbody><tr><td><strong>UI Specification</strong></td><td>Frontend &#x2F; Design</td><td>Screens, components, user flows, design tokens</td></tr><tr><td><strong>Server Specification</strong></td><td>Backend</td><td>APIs, data models, business logic, error codes</td></tr><tr><td><strong>Client Specification</strong></td><td>Android &#x2F; iOS</td><td>Platform integration, deep links, native behavior</td></tr></tbody></table><p>These are far more detailed than the Final SRS: they reference real files and modules, follow existing codebase patterns, and include concrete implementation examples rather than describing the feature in a vacuum. In practice the agent reads the Final SRS, spawns sub-agents to dig through the codebase, and writes each spec by mirroring code that already exists in the repo.</p><p>It runs as a loop:</p><table><thead><tr><th>Step</th><th>What happens</th></tr></thead><tbody><tr><td>AI generates</td><td>Claude writes the detailed implementation specs from the Final SRS</td></tr><tr><td>AI reviews</td><td>Claude reviews its own specs for correctness and completeness</td></tr><tr><td>Dev reviews</td><td>The developer validates, adjusts, and approves before any code is written</td></tr></tbody></table><p>The loop runs until the developer is satisfied — and <em>only then</em> does coding begin. Design issues get caught here, on cheap text artifacts, instead of in a PR review after the code is already written.</p><h2 id="Phase-5-—-AI-Assisted-Coding-Loop"><a href="#Phase-5-—-AI-Assisted-Coding-Loop" class="headerlink" title="Phase 5 — AI-Assisted Coding Loop"></a>Phase 5 — AI-Assisted Coding Loop</h2><p><strong>Owner: Developer + Claude.</strong></p><table><thead><tr><th>Step</th><th>What happens</th></tr></thead><tbody><tr><td>AI implements</td><td>Claude writes code based on the approved specs + Final SRS</td></tr><tr><td>AI reviews</td><td>Claude reviews the generated code for issues and edge cases</td></tr><tr><td>Dev reviews</td><td>The developer reviews, requests changes, or approves</td></tr></tbody></table><p>Because the specs and plan were already settled, this loop is mostly mechanical. The drawer feature above came together in well under an hour of wall time and roughly 3,000 lines of code — all matching a spec the whole team had already agreed on.</p><h2 id="Phase-6-—-QA-Documentation-Release"><a href="#Phase-6-—-QA-Documentation-Release" class="headerlink" title="Phase 6 — QA, Documentation &amp; Release"></a>Phase 6 — QA, Documentation &amp; Release</h2><p><strong>Owner: QA.</strong></p><ul><li>QA executes the test plan it created back in Phase 3.</li><li>Visual QA (VQA) validates the UI against the Final UI Specification.</li><li>Any issues loop back to the Dev Review stage.</li><li>Claude analyzes which <strong>docs need updating</strong> after implementation.</li><li>On pass → <strong>PROD deployment</strong>.</li></ul><p>That documentation step matters more than it looks. After the feature ships, Claude runs a documentation impact analysis: it scans which SRS files, API docs, and analytics specs drifted, classifies each as a major&#x2F;minor&#x2F;no-change update, and proposes the exact edits — so the docs that started the process stay correct for the next one.</p><p>After release, real-world feedback flows back to Product, closing the loop.</p><h2 id="What-Actually-Changes"><a href="#What-Actually-Changes" class="headerlink" title="What Actually Changes"></a>What Actually Changes</h2><p>Stripped to the essentials, D3 moves three things earlier in time:</p><ul><li><strong>The spec</strong> stops being a write-up of what was built and becomes the input that drives the build. The discussion’s output <em>is</em> the spec.</li><li><strong>QA</strong> stops testing after the fact and starts writing the test plan from the draft SRS — so test design happens before, not after, implementation.</li><li><strong>AI</strong> stops being a coding assistant at the end and becomes a participant at every phase: it gathers requirements, drafts specs, plans, implements, reviews, and flags doc drift.</li></ul><p>Everything else — predictable scope, fewer re-scoping conversations, cheaper reviews — falls out of those three shifts. The net effect is less rework and features that ship closer to what was actually intended.</p><h2 id="A-Note-on-Tooling"><a href="#A-Note-on-Tooling" class="headerlink" title="A Note on Tooling"></a>A Note on Tooling</h2><p>I describe this with Claude and a specific set of skills because that’s what we used, but <strong>D3 is model-agnostic.</strong> Nothing in the process depends on a particular vendor — it needs an LLM agent that can read your codebase, search the web, follow a multi-step prompt, and ideally call tools (a code&#x2F;docs explorer, a design integration). Any capable model fits: Claude, GPT, Gemini, or a local model behind an agent runtime like Cursor, Aider, or your own scripts.</p><p>What matters is the <em>capabilities</em>, not the brand: codebase awareness so specs reference real modules, a long enough context window to hold the consolidated requirements, and tool access so the agent validates against reality instead of hallucinating. Swap the model; the seven phases stay the same. Quality and speed will vary by model, so treat the numbers above as one data point, not a benchmark.</p><h2 id="Getting-Started"><a href="#Getting-Started" class="headerlink" title="Getting Started"></a>Getting Started</h2><ol><li><strong>Pilot on the next new feature</strong> — run D3 end-to-end as a trial.</li><li><strong>Set up the skills</strong> — <code>plan-feature</code>, <code>implement-feature</code>, and a PR-review skill.</li><li><strong>Configure the tools</strong> — code&#x2F;docs exploration and the Figma MCP.</li><li><strong>Establish SRS templates</strong> — a consistent structure for the UI &#x2F; Server &#x2F; Client specs.</li><li><strong>Review the pilot</strong> — collect feedback, measure the time saved, and refine the process.</li></ol><p>D3 is not a rigid framework — it’s a discipline. The goal is simple: by the time anyone writes code, everyone already knows exactly what we’re building.</p>]]></content>
    
    
    <summary type="html">A process where the team builds a complete, AI-assisted specification before writing any code — moving the hard thinking to the front, where fixing is cheap.</summary>
    
    
    
    <category term="AI" scheme="https://devapro.github.io/categories/AI/"/>
    
    
    <category term="ai" scheme="https://devapro.github.io/tags/ai/"/>
    
    <category term="documentation" scheme="https://devapro.github.io/tags/documentation/"/>
    
    <category term="llm" scheme="https://devapro.github.io/tags/llm/"/>
    
    <category term="claude" scheme="https://devapro.github.io/tags/claude/"/>
    
    <category term="agents" scheme="https://devapro.github.io/tags/agents/"/>
    
    <category term="srs" scheme="https://devapro.github.io/tags/srs/"/>
    
    <category term="developer-experience" scheme="https://devapro.github.io/tags/developer-experience/"/>
    
    <category term="process" scheme="https://devapro.github.io/tags/process/"/>
    
  </entry>
  
  <entry>
    <title>D3: Разработка, управляемая документацией</title>
    <link href="https://devapro.github.io/ru/2026/04/27/documentation-driven-development-ru/"/>
    <id>https://devapro.github.io/ru/2026/04/27/documentation-driven-development-ru/</id>
    <published>2026-04-27T18:00:00.000Z</published>
    <updated>2026-06-28T15:21:53.869Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>«Если фича не задокументирована — её не существует. Если она задокументирована неправильно — значит, она сломана».</p></blockquote><p>Большинство команд пишут спецификации так же, как пишут тесты к легаси-коду: задним числом, для галочки. Настоящие проектные решения рождаются в переписке в Slack, в комментарии к макету в Figma, в чьей-то голове на стендапе. К тому моменту, когда что-то наконец доходит до бумаги, код уже написан, и документ оказывается лишь описанием сделанного — а не планом того, что предстоит сделать.</p><p>«Спецификация раньше кода» — идея не новая. Подходы вроде README-Driven Development (RDD — ввёл Том Престон-Вернер в 2010 году) и «документация прежде всего» существуют больше десяти лет, и все они упирались в одно и то же: написать полную и точную спецификацию вручную долго и скучно, поэтому команды этот шаг попросту пропускали. Дисциплина была правильной — не сходилась экономика.</p><p>Изменилось вот что: теперь бо́льшую часть этой дорогой работы берёт на себя LLM-агент — он читает кодовую базу, находит пробелы, расспрашивает команду, готовит черновик спецификации, а затем по ней же пишет код. <strong>Documentation Driven Development (D3)</strong> — это та самая старая дисциплина, ставшая дешёвой: команда готовит полную спецификацию <em>до</em> написания кода, а ИИ вплетён в каждый этап, а не только в финальное кодирование. В этом посте я разбираю весь процесс целиком на примере фичи, которую мы действительно так выпустили.</p><p>Если вы читали мой предыдущий пост <a href="/ru/2026/06/27/documentation-in-the-ai-era-ru/">Документация в эпоху ИИ</a>, то D3 можно считать процессным продолжением той же мысли: если документация — это новый исходный код, то D3 — выстроенный вокруг неё процесс разработки.</p><h2 id="Какую-проблему-мы-решаем"><a href="#Какую-проблему-мы-решаем" class="headerlink" title="Какую проблему мы решаем"></a>Какую проблему мы решаем</h2><p>Симптомы вы и так знаете: спецификации, размазанные по Slack, Figma и головам сотрудников; QA, который подключается уже после того, как код написан; ИИ, к которому обращаются при кодировании, но никогда — при планировании. Не буду на этом задерживаться.</p><p>Стоит запомнить лишь одно — про экономику. Неверное предположение, замеченное на этапе спецификации, стоит одного предложения; замеченное на код-ревью — целой ветки; замеченное в проде — инцидента. Чем дольше живёт недопонимание, тем дороже его исправлять, — поэтому самый выигрышный ход — перенести самые трудные размышления как можно ближе к началу. Раньше команды так не делали по простой причине: ранние, проработанные спецификации обходились слишком дорого. Ставка D3 в том, что ИИ изменил этот расклад.</p><h2 id="Что-такое-D3"><a href="#Что-такое-D3" class="headerlink" title="Что такое D3?"></a>Что такое D3?</h2><p>D3 — это процесс, в котором команда готовит полную спецификацию фичи до написания кода. Вот что это даёт каждой стороне:</p><table><thead><tr><th>Для менеджмента</th><th>Для разработчиков</th></tr></thead><tbody><tr><td>Предсказуемые сроки — объём работ зафиксирован до начала кодирования</td><td>Вы получаете <strong>финальный SRS</strong> ещё до того, как притронетесь к коду</td></tr><tr><td>Меньше переделок — QA планирует тесты заранее</td><td>У Claude есть весь контекст — он генерирует и план, и код прямо из спецификации</td></tr><tr><td>Понятные точки передачи между Product, Engineering и QA</td><td>Цикл ревью плана отлавливает ошибки проектирования до реализации</td></tr><tr><td>ИИ усиливает команду на каждом этапе, а не только при кодировании</td><td>Никаких больше «а что Product вообще имел в виду?»</td></tr></tbody></table><p>Главный сдвиг в том, что спецификация — не бумажка, которую пишут попутно с работой. Она <em>и есть</em> работа — вплоть до момента, когда начинается генерация кода.</p><h2 id="Сквозной-процесс"><a href="#Сквозной-процесс" class="headerlink" title="Сквозной процесс"></a>Сквозной процесс</h2><p>Каждая стрелка ниже — это контрольная точка, и обратная связь может вернуться на любой из этапов.</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">Product (идея → PRD → прототип)</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">Сбор требований  ←──────── Обратная связь</span><br><span class="line">  [Аналитик/Technical Project manager + другие команды]</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">Черновик SRS ── Ревью (все стейкхолдеры + QA)</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">Финальный SRS (утверждён)  ───→  QA → Тест-план</span><br><span class="line">        │                        (разработка идёт параллельно)</span><br><span class="line">        ▼</span><br><span class="line">ИИ генерирует спеки реализации (SRS × 3)</span><br><span class="line">  UI SRS · Server SRS · Client SRS</span><br><span class="line">  [ИИ-план ↔ ИИ-ревью → Ревью разработчика]</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">ИИ-код ↔ ИИ-ревью → Ревью разработчика</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">QA / VQA → PROD → Обратная связь</span><br></pre></td></tr></table></figure><p>Пройдёмся по этапам.</p><h2 id="Этап-1-—-Идея-и-инициация"><a href="#Этап-1-—-Идея-и-инициация" class="headerlink" title="Этап 1 — Идея и инициация"></a>Этап 1 — Идея и инициация</h2><p><strong>Ответственный: Product.</strong> Этот этап остаётся таким же, как в привычной работе большинства команд. Product берёт бизнес-потребность, запрос пользователя или стратегическую цель и превращает их в связку <strong>идея → PRD → прототип</strong>. Достаточно лёгкого PRD и&#x2F;или прототипа — он напрямую питает этап сбора требований. В D3 меняется всё, что происходит <em>после</em> этого этапа.</p><h2 id="Этап-2-—-Сбор-требований"><a href="#Этап-2-—-Сбор-требований" class="headerlink" title="Этап 2 — Сбор требований"></a>Этап 2 — Сбор требований</h2><p><strong>Ответственный: Аналитик&#x2F;Technical Project manager. Время: ~20 минут, с помощью ИИ.</strong></p><p>Именно здесь ИИ впервые по-настоящему окупается. Процесс состоит из трёх шагов:</p><ol><li><strong>Свести все источники</strong> в один файл <code>.md</code> — PRD, макеты, существующую документацию, API-контракты.</li><li><strong>Claude ищет пробелы</strong> — сверяет документацию с <em>существующей кодовой базой</em>, выявляет белые пятна и собирает список открытых вопросов.</li><li><strong>Claude расспрашивает команду</strong> — задаёт точечные вопросы, чтобы закрыть пробелы, и выдаёт чистовой черновик.</li></ol><p>Важная деталь: Claude не строит догадки на основе одного лишь PRD. С помощью инструментов изучения кода и документации, а также Figma MCP он читает настоящую кодовую базу и сверяет требования с тем, что уже реализовано. Он знает, какие модули существуют, какие паттерны стоит переиспользовать и куда встроится новая фича.</p><p><img src="/images/d3/collecting-requirements.png" alt="Claude в реальном времени собирает требования — спрашивает про аналитику, проверяет спецификации и ведёт список открытых вопросов"></p><p>На скриншоте выше агент сбора требований нашёл четыре открытых вопроса по итогам изучения кодовой базы и, вместо того чтобы выдумывать ответы, обращается к команде — например, спрашивает, переиспользовать ли для нового drawer существующие события аналитики или завести собственные.</p><h2 id="Этап-3-—-Ревью-и-утверждение-финального-SRS"><a href="#Этап-3-—-Ревью-и-утверждение-финального-SRS" class="headerlink" title="Этап 3 — Ревью и утверждение финального SRS"></a>Этап 3 — Ревью и утверждение финального SRS</h2><p><strong>Ответственные: Аналитик&#x2F;Technical Project manager + QA + стейкхолдеры.</strong></p><p>Черновой SRS из этапа 2 разбирают всей командой, пока его не утвердят все. Результат — единый <strong>финальный SRS</strong>, согласованный со всеми стейкхолдерами. Как только он утверждён, сразу происходят две вещи:</p><ul><li><strong>QA пишет тест-план</strong> прямо по финальному SRS. Тесты продумываются <em>до</em> начала реализации, поэтому баги ловятся в спецификации, а не в коде.</li><li><strong>Разработка может стартовать параллельно.</strong> Финального SRS достаточно, чтобы начать; детальные спеки по доменам (следующий этап) старт не блокируют.</li></ul><p>Хороший финальный SRS содержит:</p><ul><li>Требования к фиче и критерии приёмки</li><li>Граничные случаи и обработку ошибок</li><li>API-контракты и потоки данных</li><li>Цепочки фолбэков и зависимости</li></ul><h3 id="Как-выглядит-финальный-SRS"><a href="#Как-выглядит-финальный-SRS" class="headerlink" title="Как выглядит финальный SRS"></a>Как выглядит финальный SRS</h3><p>Под абстрактные описания процесса легко кивать, поэтому вот конкретный пример (на обобщённой, обезличенной фиче). Допустим, мы добавляем <strong>drawer со списком партнёров за промо-баннером</strong>.</p><blockquote><p>В модуле <code>feature-promo-banners</code> сделать drawer со списком партнёров из API. Реализацию drawer переиспользовать из <code>feature-item-details</code>. Drawer открывается только по deep link.</p></blockquote><p><strong>Цепочка фолбэков:</strong></p><ol><li><strong>Основной сценарий:</strong> запустить промо-действие (основной партнёрский поток).</li><li><strong>Фолбэк 1:</strong> открыть Quick Access Drawer.</li><li><strong>Фолбэк 2:</strong> перейти на экран «Все партнёры».</li></ol><p><strong>Критерии приёмки:</strong></p><ul><li>Если основной промо-API падает (используется захардкоженный конфиг) — продолжать показывать скелетон и вызвать Drawer API.</li><li>Если Drawer API отвечает успешно → показать баннер; по тапу открывается Quick Access Drawer.</li><li>Если Drawer API тоже падает → показать баннер; по тапу — переход на <code>/partners</code>.</li></ul><p>Именно на таком уровне детализации команда договаривается до того, как LLM сгенерирует хоть строчку спецификации реализации или кода. Не остаётся ни одной неоднозначности, которую разработчику — или модели — пришлось бы домысливать.</p><h2 id="Этап-4-—-Спеки-реализации-сгенерированные-ИИ-SRS-×-3"><a href="#Этап-4-—-Спеки-реализации-сгенерированные-ИИ-SRS-×-3" class="headerlink" title="Этап 4 — Спеки реализации, сгенерированные ИИ (SRS × 3)"></a>Этап 4 — Спеки реализации, сгенерированные ИИ (SRS × 3)</h2><p><strong>Ответственные: разработчик + Claude.</strong></p><p>Финальный SRS говорит, <em>что</em> делать, но ещё не описывает, <em>как</em>. Это следующий шаг — и здесь за дело берётся LLM. С помощью скилла <code>plan-feature</code> (плюс инструменты изучения кода и документации и Figma MCP) Claude разворачивает финальный SRS в три детальные спецификации реализации по доменам:</p><table><thead><tr><th>Документ</th><th>Ответственный</th><th>За что отвечает</th></tr></thead><tbody><tr><td><strong>UI-спецификация</strong></td><td>Frontend &#x2F; Design</td><td>Экраны, компоненты, пользовательские сценарии, дизайн-токены</td></tr><tr><td><strong>Server-спецификация</strong></td><td>Backend</td><td>API, модели данных, бизнес-логика, коды ошибок</td></tr><tr><td><strong>Client-спецификация</strong></td><td>Android &#x2F; iOS</td><td>Платформенная интеграция, deep links, нативное поведение</td></tr></tbody></table><p>Эти документы гораздо детальнее финального SRS: они ссылаются на реальные файлы и модули, следуют существующим паттернам кодовой базы и содержат конкретные примеры реализации, а не описывают фичу в вакууме. На практике агент читает финальный SRS, запускает суб-агентов, чтобы покопаться в кодовой базе, и пишет каждую спецификацию, повторяя структуру кода, который уже есть в репозитории.</p><p>Этап идёт циклом:</p><table><thead><tr><th>Шаг</th><th>Что происходит</th></tr></thead><tbody><tr><td>ИИ генерирует</td><td>Claude пишет детальные спеки реализации из финального SRS</td></tr><tr><td>ИИ ревьюит</td><td>Claude проверяет собственные спеки на корректность и полноту</td></tr><tr><td>Разработчик ревьюит</td><td>Разработчик проверяет, правит и утверждает их до написания кода</td></tr></tbody></table><p>Цикл повторяется, пока разработчика всё не устроит, — и <em>только тогда</em> начинается кодирование. Ошибки проектирования отлавливаются здесь, на дешёвых текстовых артефактах, а не на ревью PR, когда код уже написан.</p><h2 id="Этап-5-—-Цикл-кодирования-с-ИИ"><a href="#Этап-5-—-Цикл-кодирования-с-ИИ" class="headerlink" title="Этап 5 — Цикл кодирования с ИИ"></a>Этап 5 — Цикл кодирования с ИИ</h2><p><strong>Ответственные: разработчик + Claude.</strong></p><table><thead><tr><th>Шаг</th><th>Что происходит</th></tr></thead><tbody><tr><td>ИИ пишет код</td><td>Claude реализует фичу по утверждённым спекам и финальному SRS</td></tr><tr><td>ИИ ревьюит</td><td>Claude проверяет получившийся код на ошибки и граничные случаи</td></tr><tr><td>Разработчик ревьюит</td><td>Разработчик проверяет код, просит правки или утверждает</td></tr></tbody></table><p>Поскольку спеки и план уже согласованы, этот цикл по большей части механический. Ту самую фичу с drawer собрали заметно меньше чем за час реального времени и примерно в три тысячи строк кода — и всё это соответствует спецификации, которую вся команда уже согласовала.</p><h2 id="Этап-6-—-QA-документация-и-релиз"><a href="#Этап-6-—-QA-документация-и-релиз" class="headerlink" title="Этап 6 — QA, документация и релиз"></a>Этап 6 — QA, документация и релиз</h2><p><strong>Ответственный: QA.</strong></p><ul><li>QA прогоняет тест-план, который сам же и составил ещё на этапе 3.</li><li>Визуальный QA (VQA) сверяет UI с финальной UI-спецификацией.</li><li>Любые найденные проблемы возвращаются на этап ревью разработчика.</li><li>Claude анализирует, какую <strong>документацию нужно обновить</strong> после реализации.</li><li>Если всё проходит → <strong>деплой в PROD</strong>.</li></ul><p>Шаг с документацией важнее, чем кажется. После выхода фичи Claude проводит анализ влияния на документацию: проверяет, какие файлы SRS, API-документация и спецификации аналитики разошлись с реальностью, помечает каждое изменение как крупное, мелкое или ненужное и предлагает конкретные правки — чтобы документация, с которой всё началось, оставалась актуальной к следующему разу.</p><p>После релиза реальная обратная связь от пользователей возвращается к Product — и цикл замыкается.</p><h2 id="Что-в-итоге-меняется"><a href="#Что-в-итоге-меняется" class="headerlink" title="Что в итоге меняется"></a>Что в итоге меняется</h2><p>Если свести к сути, D3 сдвигает раньше по времени три вещи:</p><ul><li><strong>Спецификация</strong> перестаёт быть отчётом о сделанном и становится исходными данными, из которых вырастает разработка. Результат обсуждения — это и есть спецификация.</li><li><strong>QA</strong> перестаёт тестировать постфактум и начинает писать тест-план уже по черновику SRS — то есть тесты продумываются до реализации, а не после.</li><li><strong>ИИ</strong> перестаёт быть помощником-кодером в самом конце и становится участником каждого этапа: собирает требования, пишет спецификации, планирует, реализует, ревьюит и отмечает расхождения в документации.</li></ul><p>Всё остальное — предсказуемый объём, меньше разговоров «давайте всё пересмотрим», более дешёвые ревью — вытекает уже из этих трёх сдвигов. Итог: меньше переделок и фичи, которые выходят ближе к тому, что задумывалось.</p><h2 id="Несколько-слов-об-инструментах"><a href="#Несколько-слов-об-инструментах" class="headerlink" title="Несколько слов об инструментах"></a>Несколько слов об инструментах</h2><p>Я описываю всё на примере Claude и конкретного набора скиллов, потому что именно ими мы пользовались, но <strong>D3 не привязан к конкретной модели.</strong> В процессе нет ничего, что зависело бы от вендора, — нужен LLM-агент, который умеет читать вашу кодовую базу, искать в интернете, выполнять многошаговый промпт и, желательно, вызывать инструменты (для изучения кода и документации, для работы с дизайном). Подойдёт любая достаточно сильная модель: Claude, GPT, Gemini или локальная модель внутри агентного окружения вроде Cursor, Aider или ваших собственных скриптов.</p><p>Важны <em>возможности</em>, а не бренд: знание кодовой базы, чтобы спецификации ссылались на реальные модули; достаточно длинный контекст, чтобы вместить все сведённые требования; и доступ к инструментам, чтобы агент сверялся с реальностью, а не выдумывал. Поменяйте модель — семь этапов останутся прежними. Качество и скорость от модели к модели будут разными, так что считайте цифры выше одним частным примером, а не эталоном.</p><h2 id="С-чего-начать"><a href="#С-чего-начать" class="headerlink" title="С чего начать"></a>С чего начать</h2><ol><li><strong>Обкатайте на следующей новой фиче</strong> — пройдите D3 от начала до конца как пробный заход.</li><li><strong>Настройте скиллы</strong> — <code>plan-feature</code>, <code>implement-feature</code> и скилл для ревью PR.</li><li><strong>Подключите инструменты</strong> — изучение кода и документации, Figma MCP.</li><li><strong>Заведите шаблоны SRS</strong> — единую структуру для UI &#x2F; Server &#x2F; Client спецификаций.</li><li><strong>Разберите результаты пилота</strong> — соберите обратную связь, оцените сэкономленное время и доработайте процесс.</li></ol><p>D3 — это не жёсткий фреймворк, а дисциплина. Цель простая: к тому моменту, когда кто-то садится писать код, все уже точно знают, что именно мы делаем.</p>]]></content>
    
    
    <summary type="html">Процесс, в котором команда с помощью ИИ готовит полную спецификацию ещё до написания кода — самая трудная работа переносится в начало, где исправления стоят дёшево.</summary>
    
    
    
    <category term="AI" scheme="https://devapro.github.io/categories/AI/"/>
    
    
    <category term="ai" scheme="https://devapro.github.io/tags/ai/"/>
    
    <category term="documentation" scheme="https://devapro.github.io/tags/documentation/"/>
    
    <category term="llm" scheme="https://devapro.github.io/tags/llm/"/>
    
    <category term="claude" scheme="https://devapro.github.io/tags/claude/"/>
    
    <category term="agents" scheme="https://devapro.github.io/tags/agents/"/>
    
    <category term="srs" scheme="https://devapro.github.io/tags/srs/"/>
    
    <category term="developer-experience" scheme="https://devapro.github.io/tags/developer-experience/"/>
    
    <category term="process" scheme="https://devapro.github.io/tags/process/"/>
    
  </entry>
  
  <entry>
    <title>Документация в эпоху ИИ: документация — это новый исходный код</title>
    <link href="https://devapro.github.io/ru/2026/04/26/documentation-in-the-ai-era-ru/"/>
    <id>https://devapro.github.io/ru/2026/04/26/documentation-in-the-ai-era-ru/</id>
    <published>2026-04-26T17:30:00.000Z</published>
    <updated>2026-06-28T15:21:53.869Z</updated>
    
    <content type="html"><![CDATA[<p>Почти всю историю разработки документацию писали как одолжение следующему человеку, который откроет код. Сегодня первым этот код читает не человек, а LLM-агент — и читает документацию каждый раз, когда берётся за фичу, планирует рефакторинг или пишет тест. Код без документации фактически стал невидимым: модель не может рассуждать о том, чего не находит.</p><p>Но написать документацию — это только половина дела: сразу за этим всплывают две проблемы, и именно им посвящён остаток статьи.</p><ul><li><strong>Хорошую документацию не читают, когда до неё трудно добраться.</strong> Ответ на вопрос «как работает фича X?» обычно уже лежит где-то в <code>docs/</code> или в коде, но чтобы его найти, нужно знать репозиторий наизусть. Поэтому проще спросить коллегу — а коллега бросает свою работу и заново объясняет то, что в репозитории давно описано.</li><li><strong>Документация устаревает.</strong> Стоит коду измениться, как документация тихо перестаёт быть правдой. Этот сбой никогда не вылезает стектрейсом — он проявляется как «агент (или новый сотрудник) сделал не то», а такую причину куда труднее отследить и куда дороже обнаружить.</li></ul><p>Две системы ниже бьют ровно по этим проблемам: мультиагентный <strong>Chat с документацией</strong>, который делает документацию и код мгновенно доступными для запросов, и автоматический конвейер <strong>Jira → Docs</strong>, который держит их актуальными. Обе предполагают, что документация, которую стоит поддерживать, у вас <em>уже</em> есть, — они усиливают эту вложенную работу, а не заменяют её. И обе описаны достаточно конкретно, чтобы их можно было повторить.</p><h2 id="Chat-с-документацией-мультиагентные-вопросы-и-ответы-по-документации-и-коду"><a href="#Chat-с-документацией-мультиагентные-вопросы-и-ответы-по-документации-и-коду" class="headerlink" title="Chat с документацией: мультиагентные вопросы и ответы по документации и коду"></a>Chat с документацией: мультиагентные вопросы и ответы по документации и коду</h2><p>Chat с документацией напрямую решает первую проблему: он превращает уже существующую документацию в то, у чего можно спрашивать, — прямо там, где команда и так общается. Стоит подчеркнуть: это окупается именно <em>потому</em>, что документация есть. Он усиливает вложения в документацию, а не позволяет их пропустить: направьте его на пустую <code>docs/</code> — и получите разве что красноречивый способ сказать «я не знаю».</p><p>Это один «мозг» за несколькими интерфейсами: веб-приложением на рабочем месте и чат-ботом. И там, и там любой член команды может задать вопрос по кодовой базе и получить потоковый ответ со ссылками на источники. Бот начинался в Slack, но в нём нет ничего, завязанного именно на Slack: тот же бэкенд точно так же поднимает <strong>Telegram</strong>-бота, а в принципе подключается любая платформа с API для ботов (Discord, Microsoft Teams, …). Приходите к людям туда, где они уже общаются, а не заставляйте открывать ещё один инструмент.</p><p>Самое интересное здесь — то, чем Chat с документацией сознательно не является: это не RAG-конвейер на эмбеддингах. Это мультиагентная система, которая ищет по документации и коду параллельно, используя файловые инструменты, уже встроенные в Claude Agent SDK.</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Клиент (Lit + Vite)          Сервер (Express + Agent SDK)</span><br><span class="line">  UI чата, рендеринг     ──►  Агент-оркестратор</span><br><span class="line">  markdown, SSE              ├── Агент документации (дерево docs)</span><br><span class="line">                            ├── Агент кода (ripgrep по репозиторию)</span><br><span class="line">                            └── Агент-ответчик (синтез ответа)</span><br></pre></td></tr></table></figure><ul><li><strong>Оркестратор</strong> — разбирает вопрос, раздаёт подзадачи, собирает результаты воедино.</li><li><strong>Агент документации</strong> — ищет по дереву docs инструментами SDK (Read, Glob, Grep).</li><li><strong>Агент кода</strong> — гоняет ripgrep по репозиторию.</li><li><strong>Агент-ответчик</strong> — собирает финальный ответ для пользователя.</li></ul><p>На стороне клиента связка Lit + TypeScript + Vite держит вес фреймворка меньше 5 КБ и обходится без виртуального DOM; ответы приходят потоком через Server-Sent Events; markdown рендерится через <code>marked</code> + <code>DOMPurify</code> + <code>highlight.js</code>. Сервер — это Express плюс <code>@anthropic-ai/claude-agent-sdk</code>, разворачивается через Docker Compose.</p><p><strong>Почему без векторной БД?</strong> На корпусе такого размера агент, который делает grep по живой файловой системе, всегда видит актуальное состояние репозитория. Индекс эмбеддингов, наоборот, хранит снимок — и стоит коду измениться, как этот снимок устаревает, пока кто-нибудь заново не пересчитает эмбеддинги и не синхронизирует индекс. Этот цикл «посчитать — синхронизировать — снова устарело» и есть та самая нагрузка на сопровождение, которая тихо убивает большинство RAG-систем. А выигрыша от него нет: <code>Glob</code> и <code>Grep</code> и так отвечают на вопрос. Поэтому индекс мы не строим — пусть агент ищет сам. Запуск в виде чат-бота дополнительно снижает порог входа: не нужно уходить из Slack или Telegram, чтобы спросить «а как работает X?».</p><h2 id="Конвейер-автообновления-поддерживаем-мозг-в-форме"><a href="#Конвейер-автообновления-поддерживаем-мозг-в-форме" class="headerlink" title="Конвейер автообновления: поддерживаем мозг в форме"></a>Конвейер автообновления: поддерживаем мозг в форме</h2><p>Chat с документацией полезен ровно настолько, насколько актуальна документация, которую он читает, — поэтому вторая система занимается тем, что поддерживает эту документацию свежей автоматически. Workflow в GitHub Actions запускается в тот момент, когда тикет в Jira переходит из <strong>Tech Review</strong> в <strong>Ready for Dev</strong> — то есть как только зафиксирован объём работ, но ещё до того, как написана хоть строчка кода. Именно момент запуска здесь неочевиден: обычно команды обновляют документацию, когда тикет уже в Done, а значит, пишут её задним числом, спустя недели, когда контекст уже выветрился. Запуск на Ready for Dev кладёт документацию рядом со спецификацией, пока всё свежо в голове, и заодно даёт разработчику задокументированную цель, под которую он будет писать код.</p><p>Конвейер состоит из четырёх типизированных стадий, и главная идея — соизмерять стоимость модели со сложностью задачи, а не натравливать одного большого агента на всё подряд:</p><ol><li><strong>Сортировка (дешёвая модель + MCP).</strong> Модель уровня Haiku забирает тикет через automation MCP, прогоняет его через фильтры по полям и решает, нужна ли вообще документация. Багфиксы, обновления зависимостей и рефакторинги она пропускает — около $0.001 за запуск, так что 90% тикетов отсеиваются за десятую долю цента ещё до того, как проснётся дорогая модель.</li><li><strong>Основной агент.</strong> Обычная (сильная) модель работает с репозиторием кода как с рабочей директорией, поэтому сама подхватывает <code>CLAUDE.md</code> и <code>.claude/</code>. Она читает индекс документации, сопоставляет изменённые модули с документами, правит файлы и делает коммит — но пока не пушит.</li><li><strong>Агент-ревьюер.</strong> Второй проход сильной модели смотрит на <code>git diff HEAD~1</code> и проверяет правила качества: никаких бизнес-метрик, никакого пересказа кода своими словами, достаточное покрытие логики. На выходе — список замечаний в JSON.</li><li><strong>Исправление и PR.</strong> Если замечания есть, агент-исправитель вносит правки, после чего workflow пушит ветку <code>docs/jira-&lt;ticket&gt;</code> и открывает (или обновляет) PR.</li></ol><p>Фильтры, которые управляют первой стадией, — это просто данные, поэтому всю систему можно настраивать, не трогая код агентов:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// config.ts — какой тикет считаем требующим документации</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> filters = &#123;</span><br><span class="line">  <span class="attr">issueTypes</span>: [<span class="string">&quot;Task&quot;</span>],                 <span class="comment">// [] = разрешить все</span></span><br><span class="line">  <span class="attr">projects</span>: [<span class="string">&quot;AND&quot;</span>],</span><br><span class="line">  <span class="attr">requiredLabels</span>: [],</span><br><span class="line">  <span class="attr">excludedLabels</span>: [<span class="string">&quot;no-docs&quot;</span>, <span class="string">&quot;found_by_automation&quot;</span>],</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>Это тот самый паттерн, который Anthropic рекомендует в <em>Building Effective Agents</em>: сортируем дёшево, работаем сильной моделью, проверяем сильной моделью, исправляем только когда нужно. Затраты предсказуемы, сбои локальны, а каждую стадию можно тестировать отдельно — полная противоположность одному агенту, который бесконечно крутится вокруг огромного набора инструментов.</p><h2 id="Что-делает-репозиторий-понятным-для-агента"><a href="#Что-делает-репозиторий-понятным-для-агента" class="headerlink" title="Что делает репозиторий понятным для агента"></a>Что делает репозиторий понятным для агента</h2><p>Обе системы стоят на одном фундаменте: репозиторий устроен так, что агент ориентируется в нём без догадок. Основную работу делают три файла.</p><p><strong><code>CLAUDE.md</code> в корне репозитория</strong> подгружается в каждую сессию агента автоматически. Воспринимайте его как загрузочный конфиг для новичка, которым оказалась модель: коротко, по делу, без воды.</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># CLAUDE.md</span></span><br><span class="line"></span><br><span class="line"><span class="section">## Сборка и тесты</span></span><br><span class="line"><span class="bullet">-</span> Сборка: ./gradlew assembleDebug</span><br><span class="line"><span class="bullet">-</span> Тесты:  ./gradlew testDebugUnitTest</span><br><span class="line"><span class="bullet">-</span> Линт:   ./gradlew ktlintCheck</span><br><span class="line"></span><br><span class="line"><span class="section">## Соглашения</span></span><br><span class="line"><span class="bullet">-</span> Одна фича — один модуль в :features/<span class="language-xml"><span class="tag">&lt;<span class="name">name</span>&gt;</span></span></span><br><span class="line"><span class="bullet">-</span> ViewModel отдаёт единый UiState; никакого LiveData в новом коде</span><br><span class="line"><span class="bullet">-</span> Результаты сетевых запросов оборачиваем в AppResult<span class="language-xml"><span class="tag">&lt;<span class="name">T</span>&gt;</span></span>, без «голых» исключений</span><br><span class="line"></span><br><span class="line"><span class="section">## Подводные камни</span></span><br><span class="line"><span class="bullet">-</span> Экраны с WebView обязаны использовать SafeWebViewClient (см. .claude/rules/webview.md)</span><br><span class="line"><span class="bullet">-</span> После правки strings.xml запускайте ./scripts/gen-translations.sh</span><br></pre></td></tr></table></figure><p><strong><code>.claude/rules/*.md</code></strong> хранят узкие тематические правила, которые ездят вместе с кодом (по файлу на тему: <code>webview.md</code>, <code>app-result.md</code>, …). Субагент читает нужное правило как готовый чек-лист, а не выводит соглашения заново при каждом запуске.</p><p><strong>Индекс документации</strong> (<code>docs/INDEX.md</code> или формат <code>llms.txt</code>) связывает модули с их документами, чтобы первым шагом агента всегда было «посмотреть, где это лежит», а не слепой поиск. Именно этот единственный переход и позволяет каждому скилу ниже стартовать с нужного документа.</p><h2 id="Документация-питает-каждый-скил"><a href="#Документация-питает-каждый-скил" class="headerlink" title="Документация питает каждый скил"></a>Документация питает каждый скил</h2><p>Когда фундамент есть, скилы — переиспользуемые именованные процедуры, которые вызывает агент (<code>/implement-feature</code>, <code>/task-worker</code>, <code>/check-coverage</code>), — все работают по одной схеме: <strong>сначала прочитать нужный документ, потом действовать.</strong></p><ul><li><code>task-worker</code> забирает тикет из Jira, по индексу документации находит папку фичи, открывает её спецификацию и пишет план, опираясь на задокументированное поведение.</li><li><code>implement-feature</code> сначала ищет уже существующие паттерны в документации, чтобы новый код ложился в принятые соглашения, а не изобретал свои.</li><li><code>review-pr-advanced</code> запускает узкоспециализированных субагентов (архитектура, Compose, структура пакетов, тесты, производительность), и каждый берёт за основу свой <code>.claude/rules/*.md</code>.</li><li><code>check-coverage</code> гоняет JaCoCo и по документации модулей понимает, что вообще должно быть покрыто тестами, — и пишет осмысленные тесты, а не ради процента покрытия.</li></ul><p>Инвариант простой: скил хорош ровно настолько, насколько хороша документация, которую он читает. В день, когда документация начинает гнить, тихо деградирует каждый скил — незаметно, потому что код всё ещё компилируется.</p><h2 id="Идея-под-капотом-Software-3-0"><a href="#Идея-под-капотом-Software-3-0" class="headerlink" title="Идея под капотом: Software 3.0"></a>Идея под капотом: Software 3.0</h2><p>Ничто из этого не завязано на конкретный стек — всё это следствие сдвига, который чётко описали два человека.</p><p>Андрей Карпаты называет это <strong>Software 3.0</strong>: после 1.0 (код, написанный руками) и 2.0 (обученные веса) программы теперь отчасти задаются промптами на естественном языке к LLM. В такой картине код, документация, конфиги и промпты — это всё входные токены, а сама LLM — среда выполнения: контекстное окно как оперативная память, документация как диск. Практический вывод — делать репозиторий удобным для LLM: простой текст, явные соглашения, примеры вместо длинных описаний, один источник истины на каждую фичу, и хранить его в репозитории, а не закапывать в Confluence. Даже «vibe coding», термин самого Карпаты, — это не про отказ от структуры: чем чётче спецификация, тем быстрее сходятся «вайбы».</p><p>Рекомендации Anthropic для Claude Code и Agent SDK приходят к тому же с инженерной стороны: понятный структурированный контекст прямо в репозитории работает лучше изощрённых промптов, а попадания в кэш промптов резко падают, когда документация меняется по косметическим поводам, — так что аккуратный текст с низкой текучестью оказывается ещё и способом сэкономить. Оба взгляда сводятся к одному правилу: дайте агенту тот же контекст, что дали бы сильному инженеру в его первый день, и держите этот контекст в актуальном состоянии.</p><h3 id="Это-не-привязано-к-одной-модели"><a href="#Это-не-привязано-к-одной-модели" class="headerlink" title="Это не привязано к одной модели"></a>Это не привязано к одной модели</h3><p>В примерах выше используется Claude Agent SDK — просто потому, что эти системы работают на нём. Но в самом подходе нет ничего, что было бы завязано на Claude. Переносимое здесь — это архитектура:</p><ul><li><strong>Chat с документацией</strong> требует LLM, которая умеет в цикле вызывать инструменты (прочитать файл, сделать grep, решить, что читать дальше) и отдавать ответ потоком. Любая модель с function calling за любым агентным фреймворком — SDK от OpenAI, LangChain&#x2F;LangGraph, локальная модель через Ollama с обёрткой под tool use — без изменений встаёт на роли оркестратора, агента документации, агента кода и ответчика. А решение «никакой векторной БД, просто grep по живому репозиторию» от модели не зависит вовсе.</li><li><strong>Конвейер для Jira</strong> — это workflow, разнесённый по стоимости: дешёвая модель сортирует, сильная пишет, сильная проверяет. Подставьте любую пару «дешёвая&#x2F;сильная» от вашего провайдера — границы стадий, передача данных через JSON и момент запуска останутся теми же.</li><li><strong>Соглашения в репозитории</strong> переносятся легче всего. <code>CLAUDE.md</code> — это просто имя файла, который автоматически читает Claude Code; сама идея — корневой файл с контекстом, тематические файлы правил, индекс документации — работает с любым агентом. Другие инструменты читают свои аналоги (<code>AGENTS.md</code>, <code>.cursorrules</code>, <code>llms.txt</code>), и их можно сделать симлинками или генерировать из одного источника, чтобы все модели читали одну и ту же правду.</li></ul><p>Если коротко: берите модель под свой бюджет и требования к приватности. Рычаг дают документация и форма workflow, а не вендор.</p><h2 id="Итог"><a href="#Итог" class="headerlink" title="Итог"></a>Итог</h2><p>В мире Software 3.0 документация — уже не вежливая формальность, какой она была в 2010-х. Это часть программы, и устаревает она ровно в тот момент, когда код уезжает у неё из-под ног. Две описанные системы — рабочий ответ на это: Chat с документацией позволяет запрашивать этот «мозг», не таская за собой векторную БД, а конвейер Jira держит «мозг» честным, записывая документацию, пока контекст ещё свежий. Обе стоят на одном и том же дешёвом и непримечательном фундаменте — <code>CLAUDE.md</code>, несколько файлов с правилами и индекс документации — и обе окупаются на каждом следующем запуске агента.</p><h3 id="Что-почитать-дальше"><a href="#Что-почитать-дальше" class="headerlink" title="Что почитать дальше"></a>Что почитать дальше</h3><ul><li>Андрей Карпаты — <a href="https://karpathy.medium.com/software-2-0-a64152b37c35">Software 2.0</a> (2017) и его доклады «Software 3.0 &#x2F; LLM OS» (2024–2025)</li><li>Anthropic — <a href="https://www.anthropic.com/research/building-effective-agents">Building Effective Agents</a> (декабрь 2024)</li><li>Anthropic — <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code best practices</a></li><li><a href="https://modelcontextprotocol.io/">Model Context Protocol</a> — открытый протокол для подключения инструментов к LLM-агентам</li><li><a href="https://llmstxt.org/">llms.txt</a> — соглашение о машиночитаемой точке входа в репозиторий</li></ul>]]></content>
    
    
    <summary type="html">[object Object]</summary>
    
    
    
    <category term="AI" scheme="https://devapro.github.io/categories/AI/"/>
    
    
    <category term="ai" scheme="https://devapro.github.io/tags/ai/"/>
    
    <category term="documentation" scheme="https://devapro.github.io/tags/documentation/"/>
    
    <category term="llm" scheme="https://devapro.github.io/tags/llm/"/>
    
    <category term="claude" scheme="https://devapro.github.io/tags/claude/"/>
    
    <category term="agents" scheme="https://devapro.github.io/tags/agents/"/>
    
    <category term="developer-experience" scheme="https://devapro.github.io/tags/developer-experience/"/>
    
  </entry>
  
  <entry>
    <title>Documentation in the AI Era: Docs Are the New Source Code</title>
    <link href="https://devapro.github.io/en/2026/04/26/documentation-in-the-ai-era/"/>
    <id>https://devapro.github.io/en/2026/04/26/documentation-in-the-ai-era/</id>
    <published>2026-04-26T17:30:00.000Z</published>
    <updated>2026-06-28T15:21:53.869Z</updated>
    
    <content type="html"><![CDATA[<p>For most of software history, documentation was a courtesy to the next human. Today the first reader of your code is an LLM agent, and it reads your docs <em>every single time</em> it opens a feature, plans a refactor, or writes a test. Undocumented code has effectively become invisible code: the model cannot reason about what it cannot find.</p><p>But writing docs is only half the battle — two problems show up right after, and they are the ones the rest of this post is about.</p><ul><li><strong>Good docs go unread when they are hard to reach.</strong> The answer to “how does feature X work?” is usually already in <code>docs/</code> or the code, but finding it means knowing the repo by heart. So people ping a colleague instead, and that colleague stops their own work to re-explain something the repo already documents.</li><li><strong>Docs drift.</strong> The moment code changes, the docs quietly stop being true. That failure never surfaces as a stack trace — it surfaces as “the agent (or the new hire) did the wrong thing,” which is far harder to attribute and much more expensive to discover.</li></ul><p>The two systems below attack exactly these: a multi-agent <strong>Doc-Chat</strong> that makes your docs and code instantly queryable, and an automated <strong>Jira → Docs</strong> pipeline that keeps them current. Both assume you <em>already</em> have documentation worth maintaining — they amplify that investment, they don’t replace it. And both are concrete enough to copy.</p><h2 id="Doc-Chat-Multi-Agent-Q-A-Over-Docs-and-Code"><a href="#Doc-Chat-Multi-Agent-Q-A-Over-Docs-and-Code" class="headerlink" title="Doc-Chat: Multi-Agent Q&amp;A Over Docs and Code"></a>Doc-Chat: Multi-Agent Q&amp;A Over Docs and Code</h2><p>Doc-Chat directly solves the first problem: it turns existing documentation into something you can ask questions of, in the place where the team already talks. It is worth stressing that this only pays off <em>because</em> the docs exist — Doc-Chat is an amplifier on a documentation investment, not a way to skip it. Point it at an empty <code>docs/</code> and you get an eloquent way to say “I don’t know.”</p><p>It is one brain behind several interfaces: a web app at the desk and a chat bot. Both let any team member ask a question about the codebase and get a sourced, streamed answer. The bot started in Slack, but nothing about it is Slack-specific — the same backend drives a <strong>Telegram</strong> bot just as well, and in principle any platform with a bot API (Discord, Microsoft Teams, …) plugs into the same place. Meet people where they already chat instead of asking them to open one more tool.</p><p>The interesting decision is what it <em>isn’t</em>: it is intentionally <strong>not</strong> a RAG pipeline with embeddings. It is a multi-agent system that searches docs and code in parallel using the filesystem tools the Claude Agent SDK already provides.</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Client (Lit + Vite)          Server (Express + Agent SDK)</span><br><span class="line">  chat UI, markdown      ──►  Orchestrator Agent</span><br><span class="line">  rendering, SSE              ├── Docs Agent  (docs tree)</span><br><span class="line">                             ├── Code Agent  (ripgrep over repo)</span><br><span class="line">                             └── Answer Agent (synthesizes)</span><br></pre></td></tr></table></figure><ul><li><strong>Orchestrator</strong> — routes the question, fans out, merges results.</li><li><strong>Docs Agent</strong> — searches the docs tree via SDK tools (Read, Glob, Grep).</li><li><strong>Code Agent</strong> — runs ripgrep over the repo.</li><li><strong>Answer Agent</strong> — synthesizes the final user-facing response.</li></ul><p>On the client side, Lit + TypeScript + Vite keeps the framework under 5 KB with no virtual DOM; answers stream over Server-Sent Events; markdown is rendered with <code>marked</code> + <code>DOMPurify</code> + <code>highlight.js</code>. The server is Express plus <code>@anthropic-ai/claude-agent-sdk</code>, deployed via Docker Compose.</p><p><strong>Why no vector DB?</strong> On a corpus this size, an agent that greps the live filesystem always reads the <em>current</em> state of the repo. An embedding index reads a snapshot — and the moment code changes, that snapshot is stale until someone re-embeds and re-syncs it. The embed&#x2F;sync&#x2F;staleness loop is exactly the maintenance burden that quietly kills most RAG systems, and it buys you nothing when <code>Glob</code> + <code>Grep</code> already answer the question. Skip the index; let the agent search. Running it as a chat bot lowers the activation cost even further: nobody has to leave Slack or Telegram to ask “how does X work?”</p><h2 id="The-Auto-Update-Workflow-Keeping-the-Brain-Healthy"><a href="#The-Auto-Update-Workflow-Keeping-the-Brain-Healthy" class="headerlink" title="The Auto-Update Workflow: Keeping the Brain Healthy"></a>The Auto-Update Workflow: Keeping the Brain Healthy</h2><p>Doc-Chat is only as good as the docs it reads, so the second system keeps those docs fresh automatically. A GitHub Actions workflow fires the moment a Jira ticket transitions from <strong>Tech Review → Ready for Dev</strong> — that is, as soon as scope is locked and <em>before</em> a line of code is written. That timing is the non-obvious part: most teams reach for “update docs when the ticket is Done,” which means docs are written retroactively, weeks later, when nobody remembers the context. Triggering at <em>Ready for Dev</em> lands the docs alongside the spec while the reasoning is fresh, and gives the implementing engineer a documented target to build against.</p><p>The pipeline has four typed stages — and the key idea is matching model cost to task difficulty rather than throwing one big agent at everything:</p><ol><li><strong>Triage (cheap model + MCP).</strong> A Haiku-class model fetches the ticket via an automation MCP, applies field filters, and decides whether docs are even needed. It skips bug fixes, dependency bumps, and refactors — roughly $0.001 per run, so 90% of tickets are rejected for a tenth of a cent before any expensive model wakes up.</li><li><strong>Main agent.</strong> The default (strong) model runs with the code repo as its working directory, so it automatically loads <code>CLAUDE.md</code> and <code>.claude/</code>. It reads the docs index, maps changed modules to docs, edits files, and commits — but does <em>not</em> push yet.</li><li><strong>Review agent.</strong> A second strong-model pass looks at <code>git diff HEAD~1</code> and checks quality rules: no business metrics, no trivial restatement of code, adequate logic coverage. Returns a JSON list of issues.</li><li><strong>Fix &amp; PR.</strong> If issues were found, a fix agent applies corrections, then the workflow pushes to a <code>docs/jira-&lt;ticket&gt;</code> branch and opens (or updates) a PR.</li></ol><p>The filter config that gates stage 1 is just data, which makes the whole thing tunable without touching agent code:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// config.ts — what counts as a docs-worthy ticket</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> filters = &#123;</span><br><span class="line">  <span class="attr">issueTypes</span>: [<span class="string">&quot;Task&quot;</span>],                 <span class="comment">// [] = allow all</span></span><br><span class="line">  <span class="attr">projects</span>: [<span class="string">&quot;AND&quot;</span>],</span><br><span class="line">  <span class="attr">requiredLabels</span>: [],</span><br><span class="line">  <span class="attr">excludedLabels</span>: [<span class="string">&quot;no-docs&quot;</span>, <span class="string">&quot;found_by_automation&quot;</span>],</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>This is the workflow pattern Anthropic recommends in <em>Building Effective Agents</em>: triage cheap, work strong, review strong, fix only when needed. Cost stays predictable, failures stay localized, and each stage is independently testable — the opposite of a single agent looping over a giant tool surface.</p><h2 id="What-Makes-a-Repo-Agent-Readable"><a href="#What-Makes-a-Repo-Agent-Readable" class="headerlink" title="What Makes a Repo Agent-Readable"></a>What Makes a Repo Agent-Readable</h2><p>Both systems lean on the same foundation: a repo laid out so an agent can navigate it without guessing. Three files do most of the work.</p><p><strong><code>CLAUDE.md</code> at the repo root</strong> is loaded into every agent session automatically. Treat it as the boot config for a new hire who happens to be a model — terse, factual, no marketing:</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># CLAUDE.md</span></span><br><span class="line"></span><br><span class="line"><span class="section">## Build &amp; test</span></span><br><span class="line"><span class="bullet">-</span> Build:  ./gradlew assembleDebug</span><br><span class="line"><span class="bullet">-</span> Test:   ./gradlew testDebugUnitTest</span><br><span class="line"><span class="bullet">-</span> Lint:   ./gradlew ktlintCheck</span><br><span class="line"></span><br><span class="line"><span class="section">## Conventions</span></span><br><span class="line"><span class="bullet">-</span> One feature per module under :features/<span class="language-xml"><span class="tag">&lt;<span class="name">name</span>&gt;</span></span></span><br><span class="line"><span class="bullet">-</span> ViewModels expose a single UiState; no LiveData in new code</span><br><span class="line"><span class="bullet">-</span> Network results wrapped in AppResult<span class="language-xml"><span class="tag">&lt;<span class="name">T</span>&gt;</span></span>, never raw exceptions</span><br><span class="line"></span><br><span class="line"><span class="section">## Gotchas</span></span><br><span class="line"><span class="bullet">-</span> WebView screens must use SafeWebViewClient (see .claude/rules/webview.md)</span><br><span class="line"><span class="bullet">-</span> Run ./scripts/gen-translations.sh after editing strings.xml</span><br></pre></td></tr></table></figure><p><strong><code>.claude/rules/*.md</code></strong> hold narrow, topical guidelines that travel with the code (one file per concern: <code>webview.md</code>, <code>app-result.md</code>, …). Sub-agents read the relevant rule as their rubric instead of re-deriving conventions every run.</p><p><strong>A docs index</strong> (<code>docs/INDEX.md</code>, or the <code>llms.txt</code> convention) maps modules to their docs so an agent’s <em>first</em> step is always “look up where this lives,” not a blind search. This single hop is what lets every skill below start from the right document.</p><h2 id="Docs-Power-Every-Skill"><a href="#Docs-Power-Every-Skill" class="headerlink" title="Docs Power Every Skill"></a>Docs Power Every Skill</h2><p>Once that foundation exists, skills — reusable, named procedures the agent invokes (<code>/implement-feature</code>, <code>/task-worker</code>, <code>/check-coverage</code>) — all share one pattern: <strong>read the relevant doc, then act.</strong></p><ul><li><code>task-worker</code> fetches a Jira ticket, reads the docs index to find the feature folder, opens its spec, then writes a plan grounded in the documented behavior.</li><li><code>implement-feature</code> discovers existing patterns through docs first, so new code matches documented conventions instead of inventing its own.</li><li><code>review-pr-advanced</code> spawns specialist sub-agents (architecture, Compose, package structure, tests, performance), each reading the relevant <code>.claude/rules/*.md</code> as its rubric.</li><li><code>check-coverage</code> runs JaCoCo and uses module docs to understand <em>what</em> should be tested — so it writes meaningful tests, not coverage-padding ones.</li></ul><p>The invariant: every skill is only as good as the docs it consumes. The day docs rot is the day every skill silently degrades — invisibly, because the output still compiles.</p><h2 id="The-Idea-Underneath-Software-3-0"><a href="#The-Idea-Underneath-Software-3-0" class="headerlink" title="The Idea Underneath: Software 3.0"></a>The Idea Underneath: Software 3.0</h2><p>None of this is specific to one stack; it follows from a shift two people have named clearly.</p><p>Andrej Karpathy frames it as <strong>Software 3.0</strong> — after 1.0 (handwritten code) and 2.0 (learned weights), programs are now partly expressed as natural-language prompts to an LLM. In that view your code, docs, configs, and prompts are all input tokens, and the LLM is the runtime: the context window is RAM, your docs are the disk. The practical corollary is to make the repo LLM-friendly — plain text, explicit conventions, examples over prose, one source of truth per feature kept <em>in the repo</em>, not buried in Confluence. Even “vibe coding,” his own term, isn’t an argument against structure: clearer specs make the vibes converge faster.</p><p>Anthropic’s guidance for Claude Code and the Agent SDK lands in the same place from the engineering side: clear, structured, repo-resident context beats clever prompting, and prompt-cache hit rates collapse when docs churn for cosmetic reasons — so high-quality, <em>low-churn</em> writing is also a cost optimization. Both views reduce to one instruction: give the agent the same context you’d give a senior engineer on day one, and keep it true.</p><h3 id="This-isn’t-tied-to-one-model"><a href="#This-isn’t-tied-to-one-model" class="headerlink" title="This isn’t tied to one model"></a>This isn’t tied to one model</h3><p>The examples above use the Claude Agent SDK because that’s what these systems run on, but nothing in the <em>approach</em> is Claude-specific. The architecture is the portable part:</p><ul><li><strong>Doc-Chat</strong> needs an LLM that can call tools in a loop (read a file, grep, decide what to read next) and stream tokens. Any function-calling model behind any agent framework — OpenAI’s SDK, LangChain&#x2F;LangGraph, a local model via Ollama with a tool-use wrapper — slots into the orchestrator&#x2F;docs&#x2F;code&#x2F;answer roles unchanged. The “no vector DB, just grep the live repo” decision is model-independent entirely.</li><li><strong>The Jira pipeline</strong> is a cost-tiered workflow: a cheap model triages, a strong model writes, a strong model reviews. Swap in whatever cheap&#x2F;strong pair your provider offers — the stage boundaries, the JSON hand-offs, and the trigger timing don’t change.</li><li><strong>The repo conventions</strong> are the most portable of all. <code>CLAUDE.md</code> is just the filename Claude Code auto-loads; the underlying idea — a root context file, topical rule files, a docs index — works for any agent. Other tools read their own equivalents (<code>AGENTS.md</code>, <code>.cursorrules</code>, <code>llms.txt</code>), and you can symlink or generate them from a single source so every model reads the same truth.</li></ul><p>In short: pick the model that fits your budget and privacy constraints. The leverage comes from the docs and the workflow shape, not the vendor.</p><h2 id="Bottom-Line"><a href="#Bottom-Line" class="headerlink" title="Bottom Line"></a>Bottom Line</h2><p>In a Software-3.0 world, docs are not the polite afterthought they were in the 2010s — they are part of the program, and they decay the moment the code moves underneath them. The two systems here are a working answer to that: Doc-Chat lets you <em>query</em> the brain without a vector DB to maintain, and the Jira pipeline keeps the brain <em>honest</em> by writing docs while the context is still fresh. Both rest on the same cheap, unglamorous foundation — a <code>CLAUDE.md</code>, a few rules files, and a docs index — and both pay for themselves on every future agent run.</p><h3 id="Further-Reading"><a href="#Further-Reading" class="headerlink" title="Further Reading"></a>Further Reading</h3><ul><li>Andrej Karpathy — <a href="https://karpathy.medium.com/software-2-0-a64152b37c35">Software 2.0</a> (2017), and his “Software 3.0 &#x2F; LLM OS” talks (2024–2025)</li><li>Anthropic — <a href="https://www.anthropic.com/research/building-effective-agents">Building Effective Agents</a> (Dec 2024)</li><li>Anthropic — <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code best practices</a></li><li><a href="https://modelcontextprotocol.io/">Model Context Protocol</a> — the open protocol for plugging tools into LLM agents</li><li><a href="https://llmstxt.org/">llms.txt</a> — a machine-readable entry-point convention for repos</li></ul>]]></content>
    
    
    <summary type="html">Two concrete systems for keeping docs alive in an AI codebase — a multi-agent Doc-Chat with no vector DB, and an automated Jira to Docs pipeline — plus the reasoning and copy-pasteable examples behind them.</summary>
    
    
    
    <category term="AI" scheme="https://devapro.github.io/categories/AI/"/>
    
    
    <category term="ai" scheme="https://devapro.github.io/tags/ai/"/>
    
    <category term="documentation" scheme="https://devapro.github.io/tags/documentation/"/>
    
    <category term="llm" scheme="https://devapro.github.io/tags/llm/"/>
    
    <category term="claude" scheme="https://devapro.github.io/tags/claude/"/>
    
    <category term="agents" scheme="https://devapro.github.io/tags/agents/"/>
    
    <category term="developer-experience" scheme="https://devapro.github.io/tags/developer-experience/"/>
    
  </entry>
  
  <entry>
    <title>Самостоятельная настройка SIP сервера</title>
    <link href="https://devapro.github.io/ru/2025/12/29/asterisk-server-rus/"/>
    <id>https://devapro.github.io/ru/2025/12/29/asterisk-server-rus/</id>
    <published>2025-12-29T22:14:34.000Z</published>
    <updated>2026-06-28T15:21:53.868Z</updated>
    
    <content type="html"><![CDATA[<p>Если вам нужно подключить несколько SIP устройств в локальной сети или через интернет без использования коммерческого SIP сервера, вы можете это сделать на небольшом VPS или Raspberry Pi.<br>(Предполагается, что у вас уже есть VPS или Raspberry Pi и вы знаете, как пользоваться командной строкой)</p><h3 id="Шаг-1-Установка"><a href="#Шаг-1-Установка" class="headerlink" title="Шаг 1. Установка"></a>Шаг 1. Установка</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install asterisk</span><br></pre></td></tr></table></figure><p>Проверьте, что Asterisk запустился (Вы должны увидеть “active (running)”)</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl status asterisk</span><br></pre></td></tr></table></figure><h3 id="Шаг-2-Настройка-аккаунтов"><a href="#Шаг-2-Настройка-аккаунтов" class="headerlink" title="Шаг 2. Настройка аккаунтов"></a>Шаг 2. Настройка аккаунтов</h3><p>В <code>sudo nano /etc/asterisk/pjsip.conf</code></p><p>Добавьте конфигурацию:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="section">[transport-udp]</span></span><br><span class="line"><span class="attr">type</span>=transport</span><br><span class="line"><span class="attr">protocol</span>=udp</span><br><span class="line"><span class="attr">bind</span>=<span class="number">0.0</span>.<span class="number">0.0</span>:<span class="number">5060</span> <span class="comment">; вы можете изменить порт по умолчанию</span></span><br><span class="line"><span class="attr">external_media_address</span>=[IP адрес VPS]</span><br><span class="line"><span class="attr">external_signaling_address</span>=[IP адрес VPS]</span><br><span class="line"><span class="attr">local_net</span>=<span class="number">192.168</span>.<span class="number">0.0</span>/<span class="number">16</span> <span class="comment">;опционально</span></span><br><span class="line"><span class="attr">local_net</span>=<span class="number">10.0</span>.<span class="number">0.0</span>/<span class="number">8</span> <span class="comment">;опционально</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; ----- Пользователь 1 -----</span></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=endpoint</span><br><span class="line"><span class="attr">context</span>=internal</span><br><span class="line"><span class="attr">disallow</span>=all</span><br><span class="line"><span class="attr">allow</span>=ulaw</span><br><span class="line"><span class="attr">auth</span>=<span class="number">1001</span></span><br><span class="line"><span class="attr">aors</span>=<span class="number">1001</span></span><br><span class="line"><span class="attr">direct_media</span>=<span class="literal">no</span> <span class="comment">; Принудительная маршрутизация RTP через Asterisk</span></span><br><span class="line"><span class="attr">rtp_symmetric</span>=<span class="literal">yes</span> <span class="comment">; Важно для NAT</span></span><br><span class="line"><span class="attr">force_rport</span>=<span class="literal">yes</span> <span class="comment">; Важно для NAT</span></span><br><span class="line"><span class="attr">rewrite_contact</span>=<span class="literal">yes</span> <span class="comment">; Важно для NAT</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=auth</span><br><span class="line"><span class="attr">auth_type</span>=userpass</span><br><span class="line"><span class="attr">password</span>=strongpassword1</span><br><span class="line"><span class="attr">username</span>=<span class="number">1001</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=aor</span><br><span class="line"><span class="attr">max_contacts</span>=<span class="number">1</span></span><br><span class="line"><span class="attr">remove_existing</span>=<span class="literal">yes</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; ----- Пользователь 2 -----</span></span><br><span class="line"><span class="section">[1002]</span></span><br><span class="line"><span class="attr">type</span>=endpoint</span><br><span class="line"><span class="attr">context</span>=internal</span><br><span class="line"><span class="attr">disallow</span>=all</span><br><span class="line"><span class="attr">allow</span>=ulaw</span><br><span class="line"><span class="attr">auth</span>=<span class="number">1002</span></span><br><span class="line"><span class="attr">aors</span>=<span class="number">1002</span></span><br><span class="line"><span class="attr">direct_media</span>=<span class="literal">no</span> <span class="comment">; Принудительная маршрутизация RTP через Asterisk</span></span><br><span class="line"><span class="attr">rtp_symmetric</span>=<span class="literal">yes</span> <span class="comment">; Важно для NAT</span></span><br><span class="line"><span class="attr">force_rport</span>=<span class="literal">yes</span> <span class="comment">; Важно для NAT</span></span><br><span class="line"><span class="attr">rewrite_contact</span>=<span class="literal">yes</span> <span class="comment">; Важно для NAT</span></span><br><span class="line"><span class="attr">media_encryption</span>=sdes  <span class="comment">; Включить SRTP для шифрованного аудио, может не поддерживаться на старых устройствах, установите no для старых устройств или приложений. Или sdes, или dtls, или srtp</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1002]</span></span><br><span class="line"><span class="attr">type</span>=auth</span><br><span class="line"><span class="attr">auth_type</span>=userpass</span><br><span class="line"><span class="attr">password</span>=strongpassword2</span><br><span class="line"><span class="attr">username</span>=<span class="number">1002</span></span><br><span class="line"><span class="comment">;realm=[ваш домен] ; в некоторых случаях, если вы установили default-realm, нужно установить то же самое для всех клиентов</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1002]</span></span><br><span class="line"><span class="attr">type</span>=aor</span><br><span class="line"><span class="attr">max_contacts</span>=<span class="number">1</span></span><br><span class="line"><span class="attr">remove_existing</span>=<span class="literal">yes</span></span><br><span class="line"><span class="attr">qualify_frequency</span>=<span class="number">60</span></span><br><span class="line"><span class="attr">maximum_expiration</span>=<span class="number">3600</span></span><br><span class="line"><span class="attr">minimum_expiration</span>=<span class="number">60</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; Повторите, если нужно больше пользователей</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>Объяснение ключевых параметров:</strong></p><ul><li><code>type=endpoint</code> - Определяет SIP конечную точку</li><li><code>context=internal</code> - Какой контекст диалплана использовать</li><li><code>auth=1001</code> - Ссылка на секцию аутентификации</li><li><code>aors=1001</code> - Ссылка на AOR (Address of Record)</li><li><code>max_contacts=1</code> - Сколько устройств может одновременно зарегистрироваться</li><li><code>remove_existing=yes</code> - Заменяет старую регистрацию при новом входе</li><li><code>direct_media=no</code> - Принудительная маршрутизация RTP через Asterisk (полезно для NAT)</li><li><code>media_encryption=no</code> - Шифрование</li></ul><p><strong>Опции media_encryption:</strong></p><ul><li><code>no</code> - Без шифрования</li><li><code>sdes</code> - SRTP с обменом ключами SDES</li><li><code>dtls</code> - SRTP с обменом ключами DTLS</li></ul><p>Использование опционального шифрования:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">media_encryption</span>=sdes</span><br><span class="line"><span class="attr">media_encryption_optimistic</span>=<span class="literal">yes</span> <span class="comment">; Разрешить откат к незашифрованному соединению</span></span><br></pre></td></tr></table></figure><p>Определите диалплан в <code>/etc/asterisk/extensions.conf</code><br>(Например, любой пользователь может позвонить другому (например, 1001 → 1002).)</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="section">[internal]</span></span><br><span class="line"><span class="attr">exten</span> =&gt; _1XXX,<span class="number">1</span>,Dial(PJSIP/<span class="variable">$&#123;EXTEN&#125;</span>,<span class="number">20</span>)</span><br><span class="line"><span class="attr">exten</span> =&gt; _1XXX,n,Hangup()</span><br></pre></td></tr></table></figure><p>Перезагрузите Asterisk</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> asterisk -rx <span class="string">&quot;reload&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ИЛИ</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> systemctl restart asterisk</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Для проверки пользователей</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> asterisk -rvvv</span><br><span class="line"><span class="comment"># затем:</span></span><br><span class="line">pjsip show endpoints</span><br></pre></td></tr></table></figure><p>RTP медиа-порты (Опционально)<br>Вы можете уменьшить количество портов для RTP<br>в <code>/etc/asterisk/rtp.conf</code></p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[general]</span></span><br><span class="line"><span class="attr">rtpstart</span>=<span class="number">10000</span></span><br><span class="line"><span class="attr">rtpend</span>=<span class="number">20000</span></span><br></pre></td></tr></table></figure><p>Проверка логов</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> asterisk -rvvv</span><br></pre></td></tr></table></figure><p>На моём старом Ubuntu сервере мне потребовались дополнительные изменения для использования PJSIP вместо <strong>chan_sip</strong> (устаревший драйвер канала SIP). Без этих изменений настройки выше не будут работать (потому что настройки для chan_sip должны быть размещены в <code>/etc/asterisk/sip.conf</code>).</p><p>Настройка загрузки модуля PJSIP<br>в <code>/etc/asterisk/modules.conf</code> обновите список модулей для загрузки:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[modules]</span></span><br><span class="line"><span class="attr">autoload</span>=<span class="literal">yes</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; Отключить chan_sip</span></span><br><span class="line"><span class="attr">noload</span> =&gt; chan_sip.so</span><br><span class="line"></span><br><span class="line"><span class="comment">; Убедитесь, что модули PJSIP загружены</span></span><br><span class="line"><span class="attr">load</span> =&gt; res_pjsip.so</span><br><span class="line"><span class="attr">load</span> =&gt; res_pjsip_session.so</span><br><span class="line"><span class="attr">load</span> =&gt; res_pjsip_registrar.so</span><br><span class="line"><span class="attr">load</span> =&gt; res_pjsip_outbound_registration.so</span><br><span class="line"><span class="attr">load</span> =&gt; chan_pjsip.so</span><br></pre></td></tr></table></figure><h3 id="STUN"><a href="#STUN" class="headerlink" title="STUN"></a>STUN</h3><p>В файле:  <code>/etc/asterisk/rtp.conf</code></p><p>добавьте:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[general]</span></span><br><span class="line"><span class="attr">rtpstart</span>=<span class="number">10000</span></span><br><span class="line"><span class="attr">rtpend</span>=<span class="number">20000</span></span><br><span class="line"><span class="attr">stunaddr</span>=stun.l.google.com:<span class="number">19302</span>  <span class="comment">; Для обхода NAT</span></span><br></pre></td></tr></table></figure><p>Проверка клиентов:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">asterisk -rx <span class="string">&quot;pjsip show endpoints&quot;</span></span><br><span class="line">asterisk -rx <span class="string">&quot;pjsip show contacts&quot;</span></span><br></pre></td></tr></table></figure><h3 id="Отладка-проблем-медиа-согласования"><a href="#Отладка-проблем-медиа-согласования" class="headerlink" title="Отладка проблем медиа-согласования"></a>Отладка проблем медиа-согласования</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">asterisk -rvvvvv</span><br></pre></td></tr></table></figure><p>Затем в CLI:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pjsip set logger on</span><br><span class="line">core set debug 5</span><br></pre></td></tr></table></figure><p>Сделайте тестовый звонок и следите за:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">-- Called PJSIP/1002</span><br><span class="line">-- PJSIP/1002-00000001 is ringing</span><br><span class="line">[NOTICE] res_pjsip_session.c: Incompatible media format - no common codec</span><br></pre></td></tr></table></figure><p>Или:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">WARNING[xxxxx]: res_pjsip_sdp_rtp.c: No common codecs between endpoints</span><br></pre></td></tr></table></figure><h4 id="Понимание-полей-статуса"><a href="#Понимание-полей-статуса" class="headerlink" title="Понимание полей статуса"></a>Понимание полей статуса</h4><ul><li><strong>Avail</strong> - Устройство доступно (qualify успешен)</li><li><strong>Unavail</strong> - Устройство не ответило на ping qualify, <strong>но звонки всё равно могут работать</strong></li><li><strong>NonQual</strong> - Qualify не настроен</li><li><strong>Unknown</strong> - Проверки qualify ещё не выполнялись</li></ul><p><strong>Важно:</strong> Статус влияет на решения по маршрутизации звонков только если вы используете <code>qualify</code> в логике диалплана. Для базовых звонков это не имеет значения!</p><h4 id="Настройки-безопасности-PJSIP"><a href="#Настройки-безопасности-PJSIP" class="headerlink" title="Настройки безопасности PJSIP"></a>Настройки безопасности PJSIP</h4><p>В <code>/etc/asterisk/pjsip.conf</code>:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="section">[global]</span></span><br><span class="line"><span class="attr">allow_anonymous</span>=<span class="literal">no</span></span><br><span class="line"><span class="attr">max_forwards</span>=<span class="number">70</span></span><br><span class="line"><span class="attr">user_agent</span>=Asterisk PBX</span><br><span class="line"><span class="attr">default_realm</span>=[ваш домен]</span><br><span class="line"></span><br><span class="line"><span class="comment">; ACL для ограничения источников регистрации (опционально, но рекомендуется)</span></span><br><span class="line"><span class="section">[acl]</span></span><br><span class="line"><span class="attr">type</span>=acl</span><br><span class="line"><span class="attr">deny</span>=<span class="number">0.0</span>.<span class="number">0.0</span>/<span class="number">0.0</span>.<span class="number">0.0</span></span><br><span class="line"><span class="attr">permit</span>=IP_ВАШЕГО_ОФИСА/<span class="number">32</span></span><br><span class="line"><span class="attr">permit</span>=IP_ВАШЕГО_ДОМА/<span class="number">32</span></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Отключите ненужные модули в <code>/etc/asterisk/modules.conf</code></p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="section">[modules]</span></span><br><span class="line"><span class="attr">autoload</span>=<span class="literal">yes</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; Отключить chan_sip, если используете PJSIP</span></span><br><span class="line"><span class="attr">noload</span> =&gt; chan_sip.so</span><br><span class="line"></span><br><span class="line"><span class="comment">; Отключить ненужные протоколы</span></span><br><span class="line"><span class="attr">noload</span> =&gt; chan_skinny.so</span><br><span class="line"><span class="attr">noload</span> =&gt; chan_mgcp.so</span><br><span class="line"><span class="attr">noload</span> =&gt; chan_unistim.so</span><br><span class="line"></span><br><span class="line"><span class="comment">; Отключить веб-интерфейс, если не нужен</span></span><br><span class="line"><span class="attr">noload</span> =&gt; res_http_websocket.so</span><br><span class="line"><span class="attr">noload</span> =&gt; res_ari.so</span><br><span class="line"><span class="attr">noload</span> =&gt; res_stasis.so</span><br></pre></td></tr></table></figure><p>Защита Asterisk Manager Interface (AMI) в <code>/etc/asterisk/manager.conf</code></p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="section">[general]</span></span><br><span class="line"><span class="attr">enabled</span> = <span class="literal">no</span>  <span class="comment">; Отключить, если не нужен</span></span><br><span class="line"><span class="attr">port</span> = <span class="number">5038</span></span><br><span class="line"><span class="attr">bindaddr</span> = <span class="number">127.0</span>.<span class="number">0.1</span>  <span class="comment">; Только локальный доступ</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; Если вам нужен AMI, используйте сильные учётные данные:</span></span><br><span class="line"><span class="section">[admin]</span></span><br><span class="line"><span class="attr">secret</span> = VeryStr0ngAMIPass123!</span><br><span class="line"><span class="attr">deny</span> = <span class="number">0.0</span>.<span class="number">0.0</span>/<span class="number">0.0</span>.<span class="number">0.0</span></span><br><span class="line"><span class="attr">permit</span> = <span class="number">127.0</span>.<span class="number">0.1</span>/<span class="number">255.255</span>.<span class="number">255.0</span></span><br><span class="line"><span class="attr">read</span> = system,call,log,verbose,command,agent,user,config</span><br><span class="line"><span class="attr">write</span> = system,call,log,verbose,command,agent,user,config</span><br></pre></td></tr></table></figure><p>Проверка подозрительной активности:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Проверить неудачные попытки регистрации</span></span><br><span class="line">asterisk -rx <span class="string">&quot;pjsip show registrations&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Мониторинг активных каналов</span></span><br><span class="line">asterisk -rx <span class="string">&quot;core show channels&quot;</span></span><br></pre></td></tr></table></figure><h2 id="SSL"><a href="#SSL" class="headerlink" title="SSL"></a>SSL</h2><p>Вариант A: Let’s Encrypt (Бесплатно и рекомендуется)</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Установить Certbot</span></span><br><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install certbot</span><br><span class="line"></span><br><span class="line"><span class="comment"># Получить сертификат</span></span><br><span class="line"><span class="built_in">sudo</span> certbot certonly --standalone -d [ваш домен]</span><br><span class="line"></span><br><span class="line"><span class="comment"># Сертификат будет находиться по адресу:</span></span><br><span class="line"><span class="comment"># /etc/letsencrypt/live/[ваш домен]/fullchain.pem</span></span><br><span class="line"><span class="comment"># /etc/letsencrypt/live/[ваш домен]/privkey.pem</span></span><br></pre></td></tr></table></figure><p>Вариант B: Самоподписанный сертификат</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">mkdir</span> -p /etc/asterisk/keys</span><br><span class="line"><span class="built_in">cd</span> /etc/asterisk/keys</span><br><span class="line"></span><br><span class="line"><span class="comment"># Сгенерировать приватный ключ и сертификат</span></span><br><span class="line"><span class="built_in">sudo</span> openssl req -new -x509 -days 365 -nodes \</span><br><span class="line">  -out asterisk.pem -keyout asterisk.key \</span><br><span class="line">  -subj <span class="string">&quot;/C=RU/ST=State/L=City/O=Organization/CN=s.mdroid.ru&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Объединить их</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">cat</span> asterisk.key asterisk.pem &gt; asterisk.combined.pem</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 600 asterisk.combined.pem</span><br></pre></td></tr></table></figure><p>Установка правильных прав доступа</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Для сертификатов Let&#x27;s Encrypt</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chown</span> -R asterisk:asterisk /etc/letsencrypt/</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> -R 640 /etc/letsencrypt/live/s.mdroid.ru/*.pem</span><br><span class="line"></span><br><span class="line"><span class="comment"># Для самоподписанных сертификатов</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chown</span> asterisk:asterisk /etc/asterisk/keys/*</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 600 /etc/asterisk/keys/*</span><br></pre></td></tr></table></figure><h4 id="Настройка"><a href="#Настройка" class="headerlink" title="Настройка"></a>Настройка</h4><p>В файле: <code>/etc/asterisk/pjsip.conf</code></p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[global]</span></span><br><span class="line"><span class="attr">max_forwards</span>=<span class="number">70</span></span><br><span class="line"><span class="attr">user_agent</span>=Asterisk PBX</span><br><span class="line"><span class="attr">default_realm</span>=[ваш домен или asterisk]</span><br><span class="line"></span><br><span class="line"><span class="comment">; UDP транспорт (оставьте для обратной совместимости)</span></span><br><span class="line"><span class="section">[transport-udp]</span></span><br><span class="line">....</span><br><span class="line"></span><br><span class="line"><span class="comment">; TLS транспорт (безопасный)</span></span><br><span class="line"><span class="section">[transport-tls]</span></span><br><span class="line"><span class="attr">type</span>=transport</span><br><span class="line"><span class="attr">protocol</span>=tls</span><br><span class="line"><span class="attr">bind</span>=<span class="number">0.0</span>.<span class="number">0.0</span>:<span class="number">5061</span></span><br><span class="line"><span class="attr">cert_file</span>=/etc/letsencrypt/live/[ваш домен]/fullchain.pem</span><br><span class="line"><span class="attr">priv_key_file</span>=/etc/letsencrypt/live/[ваш домен]/privkey.pem</span><br><span class="line"><span class="comment">; ИЛИ для самоподписанного:</span></span><br><span class="line"><span class="comment">; cert_file=/etc/asterisk/keys/asterisk.combined.pem</span></span><br><span class="line"><span class="comment">; priv_key_file=/etc/asterisk/keys/asterisk.combined.pem</span></span><br><span class="line"><span class="comment">;cipher=ALL ; используйте только если вы знаете, какой формат поддерживается</span></span><br><span class="line"><span class="attr">method</span>=sslv23 <span class="comment">; или tlsv1_2 для современных устройств</span></span><br><span class="line"><span class="attr">verify_server</span>=<span class="literal">no</span></span><br><span class="line"><span class="attr">verify_client</span>=<span class="literal">no</span></span><br><span class="line"><span class="attr">external_media_address</span>=[IP сервера]</span><br><span class="line"><span class="attr">external_signaling_address</span>=[IP сервера]</span><br><span class="line"></span><br><span class="line"><span class="comment">; Обновите конечные точки для использования TLS</span></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=endpoint</span><br><span class="line"><span class="attr">context</span>=internal</span><br><span class="line"><span class="comment">;transport=transport-tls ;опционально, чтобы принудительно использовать TLS</span></span><br><span class="line"><span class="attr">disallow</span>=all</span><br><span class="line"><span class="attr">allow</span>=ulaw</span><br><span class="line"><span class="attr">allow</span>=alaw</span><br><span class="line"><span class="attr">auth</span>=<span class="number">1001</span></span><br><span class="line"><span class="attr">aors</span>=<span class="number">1001</span></span><br><span class="line"><span class="attr">direct_media</span>=<span class="literal">no</span></span><br><span class="line"><span class="attr">rtp_symmetric</span>=<span class="literal">yes</span></span><br><span class="line"><span class="attr">force_rport</span>=<span class="literal">yes</span></span><br><span class="line"><span class="attr">rewrite_contact</span>=<span class="literal">yes</span></span><br><span class="line"><span class="attr">media_encryption</span>=sdes  <span class="comment">; Включить SRTP для шифрованного аудио, может не поддерживаться на старых устройствах, установите no для старых устройств или приложений. Или sdes, или dtls</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=auth</span><br><span class="line"><span class="attr">auth_type</span>=userpass</span><br><span class="line"><span class="attr">password</span>=YourStrongPassword</span><br><span class="line"><span class="attr">username</span>=<span class="number">1001</span></span><br><span class="line"><span class="comment">;realm=[ваш домен] ; в некоторых случаях, если вы установили default-realm, нужно установить то же самое для всех клиентов</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=aor</span><br><span class="line"><span class="attr">max_contacts</span>=<span class="number">1</span></span><br><span class="line"><span class="attr">remove_existing</span>=<span class="literal">yes</span></span><br><span class="line"><span class="attr">qualify_frequency</span>=<span class="number">60</span></span><br></pre></td></tr></table></figure><p>Перезагрузка:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Перезагрузка</span></span><br><span class="line">asterisk -rx <span class="string">&quot;core reload&quot;</span></span><br><span class="line">asterisk -rx <span class="string">&quot;pjsip reload&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Проверка работы TLS транспорта</span></span><br><span class="line">asterisk -rx <span class="string">&quot;pjsip show transports&quot;</span></span><br></pre></td></tr></table></figure><p>Вы должны увидеть вывод вроде:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Transport:  &lt;TransportId........&gt;  &lt;Type&gt;  &lt;cos&gt;  &lt;tos&gt;  &lt;BindAddress.....................&gt;</span><br><span class="line">===========================================================================</span><br><span class="line">transport-udp               udp      0      0  0.0.0.0:5060</span><br><span class="line">transport-tls               tls      0      0  0.0.0.0:5061</span><br></pre></td></tr></table></figure><p>Если вы не видите <code>transport-tls</code>, проверьте логи:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">tail</span> -50 /var/log/asterisk/full | grep -i transport</span><br></pre></td></tr></table></figure><p>Если вы видите ошибку:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ERROR[1069851] res_sorcery_config.c: Could not create an object of type &#x27;transport&#x27; with id &#x27;transport-tls&#x27; from configuration file &#x27;pjsip.conf&#x27;</span><br></pre></td></tr></table></figure><p>Проверьте:</p><ul><li>Что путь к файлам сертификатов правильный</li><li>Удалите или измените параметр cipher (у меня работает: <code>cipher=ADH-AES256-SHA,ADH-AES128-SHA</code>)</li></ul><p>Для самоподписанных сертификатов вам нужно отключить проверку сертификата на клиенте или добавить в <code>/etc/asterisk/pjsip.conf</code>:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[transport-tls]</span></span><br><span class="line">....</span><br><span class="line"><span class="attr">require_client_cert</span>=<span class="literal">no</span></span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">Настройка самостоятельного SIP сервера с использованием Asterisk</summary>
    
    
    
    <category term="Linux" scheme="https://devapro.github.io/categories/Linux/"/>
    
    
    <category term="linux" scheme="https://devapro.github.io/tags/linux/"/>
    
    <category term="self-hosted" scheme="https://devapro.github.io/tags/self-hosted/"/>
    
    <category term="sip" scheme="https://devapro.github.io/tags/sip/"/>
    
  </entry>
  
  <entry>
    <title>How to Set JAVA_HOME Environment Variable</title>
    <link href="https://devapro.github.io/en/2025/12/29/java_home/"/>
    <id>https://devapro.github.io/en/2025/12/29/java_home/</id>
    <published>2025-12-29T16:45:00.000Z</published>
    <updated>2026-06-28T15:21:53.869Z</updated>
    
    <content type="html"><![CDATA[<p>JAVA_HOME is an environment variable that points to your Java installation directory. Many Java-based applications and build tools (Maven, Gradle, Tomcat) require this variable to be set correctly.</p><h2 id="Why-Set-JAVA-HOME"><a href="#Why-Set-JAVA-HOME" class="headerlink" title="Why Set JAVA_HOME?"></a>Why Set JAVA_HOME?</h2><ul><li><strong>Required by build tools</strong>: Maven, Gradle, and Ant need it to find Java</li><li><strong>Application servers</strong>: Tomcat, JBoss, and others use it</li><li><strong>IDE compatibility</strong>: Ensures consistent Java version across tools</li><li><strong>Script automation</strong>: Makes Java location consistent across systems</li></ul><h2 id="Finding-Your-Java-Installation"><a href="#Finding-Your-Java-Installation" class="headerlink" title="Finding Your Java Installation"></a>Finding Your Java Installation</h2><h3 id="Linux"><a href="#Linux" class="headerlink" title="Linux"></a>Linux</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Find Java installation path</span></span><br><span class="line"><span class="built_in">which</span> java</span><br><span class="line"></span><br><span class="line"><span class="comment"># Get full path (follows symlinks)</span></span><br><span class="line"><span class="built_in">readlink</span> -f $(<span class="built_in">which</span> java)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Or use update-alternatives</span></span><br><span class="line">update-alternatives --list java</span><br></pre></td></tr></table></figure><p>Common locations:</p><ul><li><code>/usr/lib/jvm/java-11-openjdk-amd64</code></li><li><code>/usr/lib/jvm/java-17-openjdk-amd64</code></li><li><code>/usr/java/jdk-17.0.1</code></li></ul><h3 id="macOS"><a href="#macOS" class="headerlink" title="macOS"></a>macOS</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Find Java home using java_home utility</span></span><br><span class="line">/usr/libexec/java_home</span><br><span class="line"></span><br><span class="line"><span class="comment"># List all installed Java versions</span></span><br><span class="line">/usr/libexec/java_home -V</span><br><span class="line"></span><br><span class="line"><span class="comment"># Get path for specific version</span></span><br><span class="line">/usr/libexec/java_home -v 17</span><br></pre></td></tr></table></figure><p>Common locations:</p><ul><li><code>/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home</code></li><li><code>/Applications/Android Studio.app/Contents/jbr/Contents/Home</code></li></ul><h3 id="Windows"><a href="#Windows" class="headerlink" title="Windows"></a>Windows</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"># From Command Prompt</span><br><span class="line">where java</span><br><span class="line"></span><br><span class="line"># From PowerShell</span><br><span class="line">(Get-Command java).<span class="built_in">Path</span></span><br></pre></td></tr></table></figure><p>Common locations:</p><ul><li><code>C:\Program Files\Java\jdk-17</code></li><li><code>C:\Program Files\OpenJDK\jdk-17.0.1</code></li><li><code>C:\Program Files\Eclipse Adoptium\jdk-17.0.1-hotspot</code></li></ul><h2 id="Setting-JAVA-HOME"><a href="#Setting-JAVA-HOME" class="headerlink" title="Setting JAVA_HOME"></a>Setting JAVA_HOME</h2><h3 id="Linux-1"><a href="#Linux-1" class="headerlink" title="Linux"></a>Linux</h3><h4 id="Temporary-Current-Session-Only"><a href="#Temporary-Current-Session-Only" class="headerlink" title="Temporary (Current Session Only)"></a>Temporary (Current Session Only)</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PATH</span>:<span class="variable">$JAVA_HOME</span>/bin</span><br></pre></td></tr></table></figure><h4 id="Permanent-All-Sessions"><a href="#Permanent-All-Sessions" class="headerlink" title="Permanent (All Sessions)"></a>Permanent (All Sessions)</h4><p>Add to <code>~/.bashrc</code> or <code>~/.zshrc</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Open your shell config file</span></span><br><span class="line">nano ~/.bashrc  <span class="comment"># or ~/.zshrc for Zsh</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Add these lines at the end</span></span><br><span class="line"><span class="built_in">export</span> JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PATH</span>:<span class="variable">$JAVA_HOME</span>/bin</span><br><span class="line"></span><br><span class="line"><span class="comment"># Save and reload</span></span><br><span class="line"><span class="built_in">source</span> ~/.bashrc  <span class="comment"># or source ~/.zshrc</span></span><br></pre></td></tr></table></figure><h4 id="System-wide-All-Users"><a href="#System-wide-All-Users" class="headerlink" title="System-wide (All Users)"></a>System-wide (All Users)</h4><p>Create a file in <code>/etc/profile.d/</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Create environment file</span></span><br><span class="line"><span class="built_in">sudo</span> nano /etc/profile.d/java.sh</span><br><span class="line"></span><br><span class="line"><span class="comment"># Add this content</span></span><br><span class="line"><span class="built_in">export</span> JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PATH</span>:<span class="variable">$JAVA_HOME</span>/bin</span><br><span class="line"></span><br><span class="line"><span class="comment"># Make it executable</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> +x /etc/profile.d/java.sh</span><br><span class="line"></span><br><span class="line"><span class="comment"># Reload</span></span><br><span class="line"><span class="built_in">source</span> /etc/profile.d/java.sh</span><br></pre></td></tr></table></figure><h3 id="macOS-1"><a href="#macOS-1" class="headerlink" title="macOS"></a>macOS</h3><h4 id="Using-Shell-Configuration"><a href="#Using-Shell-Configuration" class="headerlink" title="Using Shell Configuration"></a>Using Shell Configuration</h4><p>Add to <code>~/.zshrc</code> (default shell on modern macOS):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Open Zsh config</span></span><br><span class="line">nano ~/.zshrc</span><br><span class="line"></span><br><span class="line"><span class="comment"># Add these lines</span></span><br><span class="line"><span class="built_in">export</span> JAVA_HOME=$(/usr/libexec/java_home)</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$JAVA_HOME</span>/bin:<span class="variable">$PATH</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Save and reload</span></span><br><span class="line"><span class="built_in">source</span> ~/.zshrc</span><br></pre></td></tr></table></figure><h4 id="For-Specific-Java-Version"><a href="#For-Specific-Java-Version" class="headerlink" title="For Specific Java Version"></a>For Specific Java Version</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Java 17</span></span><br><span class="line"><span class="built_in">export</span> JAVA_HOME=$(/usr/libexec/java_home -v 17)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Java 11</span></span><br><span class="line"><span class="built_in">export</span> JAVA_HOME=$(/usr/libexec/java_home -v 11)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Java 1.8</span></span><br><span class="line"><span class="built_in">export</span> JAVA_HOME=$(/usr/libexec/java_home -v 1.8)</span><br></pre></td></tr></table></figure><h4 id="For-Android-Studio’s-JDK"><a href="#For-Android-Studio’s-JDK" class="headerlink" title="For Android Studio’s JDK"></a>For Android Studio’s JDK</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> JAVA_HOME=<span class="string">&quot;/Applications/Android Studio.app/Contents/jbr/Contents/Home&quot;</span></span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$JAVA_HOME</span>/bin:<span class="variable">$PATH</span></span><br></pre></td></tr></table></figure><h3 id="Windows-1"><a href="#Windows-1" class="headerlink" title="Windows"></a>Windows</h3><h4 id="Using-System-Properties-GUI"><a href="#Using-System-Properties-GUI" class="headerlink" title="Using System Properties (GUI)"></a>Using System Properties (GUI)</h4><ol><li>Open <strong>Start Menu</strong>, search for “Environment Variables”</li><li>Click <strong>Edit the system environment variables</strong></li><li>Click <strong>Environment Variables</strong> button</li><li>Under <strong>System variables</strong>, click <strong>New</strong></li><li>Set:<ul><li>Variable name: <code>JAVA_HOME</code></li><li>Variable value: <code>C:\Program Files\Java\jdk-17</code> (your Java path)</li></ul></li><li>Click <strong>OK</strong></li><li>Find <strong>Path</strong> variable, click <strong>Edit</strong></li><li>Click <strong>New</strong> and add: <code>%JAVA_HOME%\bin</code></li><li>Click <strong>OK</strong> on all dialogs</li><li>Restart any open Command Prompts</li></ol><h4 id="Using-Command-Prompt-Admin"><a href="#Using-Command-Prompt-Admin" class="headerlink" title="Using Command Prompt (Admin)"></a>Using Command Prompt (Admin)</h4><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">setx JAVA_HOME &quot;C:\Program Files\Java\jdk-<span class="number">17</span>&quot; /M</span><br><span class="line">setx <span class="built_in">PATH</span> &quot;<span class="variable">%PATH%</span>;<span class="variable">%JAVA_HOME%</span>\bin&quot; /M</span><br></pre></td></tr></table></figure><p><strong>Note</strong>: <code>/M</code> sets system-wide. Remove it for user-only.</p><h4 id="Using-PowerShell-Admin"><a href="#Using-PowerShell-Admin" class="headerlink" title="Using PowerShell (Admin)"></a>Using PowerShell (Admin)</h4><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[<span class="type">System.Environment</span>]::SetEnvironmentVariable(<span class="string">&quot;JAVA_HOME&quot;</span>, <span class="string">&quot;C:\Program Files\Java\jdk-17&quot;</span>, <span class="string">&quot;Machine&quot;</span>)</span><br><span class="line"><span class="variable">$path</span> = [<span class="type">System.Environment</span>]::GetEnvironmentVariable(<span class="string">&quot;PATH&quot;</span>, <span class="string">&quot;Machine&quot;</span>)</span><br><span class="line">[<span class="type">System.Environment</span>]::SetEnvironmentVariable(<span class="string">&quot;PATH&quot;</span>, <span class="string">&quot;<span class="variable">$path</span>;%JAVA_HOME%\bin&quot;</span>, <span class="string">&quot;Machine&quot;</span>)</span><br></pre></td></tr></table></figure><h2 id="Verify-Installation"><a href="#Verify-Installation" class="headerlink" title="Verify Installation"></a>Verify Installation</h2><p>After setting JAVA_HOME, verify it’s working:</p><h3 id="Linux-macOS"><a href="#Linux-macOS" class="headerlink" title="Linux&#x2F;macOS"></a>Linux&#x2F;macOS</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Check JAVA_HOME value</span></span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$JAVA_HOME</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Verify Java is accessible</span></span><br><span class="line">java -version</span><br><span class="line"></span><br><span class="line"><span class="comment"># Verify javac (compiler) is accessible</span></span><br><span class="line">javac -version</span><br><span class="line"></span><br><span class="line"><span class="comment"># Full test</span></span><br><span class="line"><span class="variable">$JAVA_HOME</span>/bin/java -version</span><br></pre></td></tr></table></figure><p>Expected output:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">openjdk version &quot;17.0.1&quot; 2021-10-19</span><br><span class="line">OpenJDK Runtime Environment (build 17.0.1+12)</span><br><span class="line">OpenJDK 64-Bit Server VM (build 17.0.1+12, mixed mode)</span><br></pre></td></tr></table></figure><h3 id="Windows-2"><a href="#Windows-2" class="headerlink" title="Windows"></a>Windows</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"># Check JAVA_HOME value</span><br><span class="line"><span class="built_in">echo</span> <span class="variable">%JAVA_HOME%</span></span><br><span class="line"></span><br><span class="line"># <span class="built_in">Verify</span> Java is accessible</span><br><span class="line">java -version</span><br><span class="line"></span><br><span class="line"># <span class="built_in">Verify</span> javac (compiler) is accessible</span><br><span class="line">javac -version</span><br></pre></td></tr></table></figure><h2 id="Common-Issues"><a href="#Common-Issues" class="headerlink" title="Common Issues"></a>Common Issues</h2><h3 id="“JAVA-HOME-is-not-defined”"><a href="#“JAVA-HOME-is-not-defined”" class="headerlink" title="“JAVA_HOME is not defined”"></a>“JAVA_HOME is not defined”</h3><p><strong>Solution</strong>: Ensure you’ve reloaded your shell or restarted your terminal after setting the variable.</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Linux/macOS - reload config</span></span><br><span class="line"><span class="built_in">source</span> ~/.bashrc  <span class="comment"># or ~/.zshrc</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Windows - restart Command Prompt/PowerShell</span></span><br></pre></td></tr></table></figure><h3 id="“command-not-found-java”-after-setting-JAVA-HOME"><a href="#“command-not-found-java”-after-setting-JAVA-HOME" class="headerlink" title="“command not found: java” after setting JAVA_HOME"></a>“command not found: java” after setting JAVA_HOME</h3><p><strong>Solution</strong>: Make sure you also added <code>$JAVA_HOME/bin</code> to your PATH.</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Check if JAVA_HOME/bin is in PATH</span></span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$PATH</span> | grep <span class="string">&quot;<span class="variable">$JAVA_HOME</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># If not, add it</span></span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PATH</span>:<span class="variable">$JAVA_HOME</span>/bin</span><br></pre></td></tr></table></figure><h3 id="Wrong-Java-version-being-used"><a href="#Wrong-Java-version-being-used" class="headerlink" title="Wrong Java version being used"></a>Wrong Java version being used</h3><p><strong>Solution</strong>: Check if multiple Java installations exist and PATH order.</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Find all Java locations</span></span><br><span class="line"><span class="built_in">which</span> -a java</span><br><span class="line"></span><br><span class="line"><span class="comment"># Check PATH order</span></span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$PATH</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Ensure JAVA_HOME/bin appears early in PATH</span></span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$JAVA_HOME</span>/bin:<span class="variable">$PATH</span>  <span class="comment"># Note: JAVA_HOME/bin comes FIRST</span></span><br></pre></td></tr></table></figure><h3 id="Maven-Gradle-still-can’t-find-Java"><a href="#Maven-Gradle-still-can’t-find-Java" class="headerlink" title="Maven&#x2F;Gradle still can’t find Java"></a>Maven&#x2F;Gradle still can’t find Java</h3><p><strong>Solution</strong>: Some tools need explicit JAVA_HOME. Verify the path points to JDK, not JRE:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Should show JDK directory with &#x27;bin&#x27;, &#x27;lib&#x27;, &#x27;include&#x27;, etc.</span></span><br><span class="line"><span class="built_in">ls</span> <span class="variable">$JAVA_HOME</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Must contain javac (Java compiler)</span></span><br><span class="line"><span class="built_in">ls</span> <span class="variable">$JAVA_HOME</span>/bin/javac</span><br></pre></td></tr></table></figure><h2 id="Multiple-Java-Versions"><a href="#Multiple-Java-Versions" class="headerlink" title="Multiple Java Versions"></a>Multiple Java Versions</h2><p>If you need to switch between Java versions:</p><h3 id="Linux-Using-update-alternatives"><a href="#Linux-Using-update-alternatives" class="headerlink" title="Linux - Using update-alternatives"></a>Linux - Using update-alternatives</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># List available Java versions</span></span><br><span class="line"><span class="built_in">sudo</span> update-alternatives --config java</span><br><span class="line"></span><br><span class="line"><span class="comment"># Select the version you want by entering the number</span></span><br></pre></td></tr></table></figure><h3 id="macOS-Using-jenv"><a href="#macOS-Using-jenv" class="headerlink" title="macOS - Using jenv"></a>macOS - Using jenv</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Install jenv</span></span><br><span class="line">brew install jenv</span><br><span class="line"></span><br><span class="line"><span class="comment"># Add to shell config</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;export PATH=&quot;$HOME/.jenv/bin:$PATH&quot;&#x27;</span> &gt;&gt; ~/.zshrc</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;eval &quot;$(jenv init -)&quot;&#x27;</span> &gt;&gt; ~/.zshrc</span><br><span class="line"></span><br><span class="line"><span class="comment"># Add Java versions</span></span><br><span class="line">jenv add /Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home</span><br><span class="line">jenv add /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home</span><br><span class="line"></span><br><span class="line"><span class="comment"># Set global version</span></span><br><span class="line">jenv global 17</span><br><span class="line"></span><br><span class="line"><span class="comment"># Set local version (project-specific)</span></span><br><span class="line"><span class="built_in">cd</span> your-project</span><br><span class="line">jenv <span class="built_in">local</span> 11</span><br></pre></td></tr></table></figure><h3 id="Windows-Manual-switching"><a href="#Windows-Manual-switching" class="headerlink" title="Windows - Manual switching"></a>Windows - Manual switching</h3><p>Create batch files for each version:</p><p><strong>java11.bat:</strong></p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">@<span class="built_in">echo</span> off</span><br><span class="line">setx JAVA_HOME &quot;C:\Program Files\Java\jdk-<span class="number">11</span>&quot; /M</span><br><span class="line"><span class="built_in">echo</span> Java <span class="number">11</span> is now active</span><br></pre></td></tr></table></figure><p><strong>java17.bat:</strong></p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">@<span class="built_in">echo</span> off</span><br><span class="line">setx JAVA_HOME &quot;C:\Program Files\Java\jdk-<span class="number">17</span>&quot; /M</span><br><span class="line"><span class="built_in">echo</span> Java <span class="number">17</span> is now active</span><br></pre></td></tr></table></figure><h2 id="Quick-Reference"><a href="#Quick-Reference" class="headerlink" title="Quick Reference"></a>Quick Reference</h2><h3 id="Linux-macOS-1"><a href="#Linux-macOS-1" class="headerlink" title="Linux&#x2F;macOS"></a>Linux&#x2F;macOS</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Set JAVA_HOME temporarily</span></span><br><span class="line"><span class="built_in">export</span> JAVA_HOME=/path/to/java</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$JAVA_HOME</span>/bin:<span class="variable">$PATH</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Set JAVA_HOME permanently (add to ~/.bashrc or ~/.zshrc)</span></span><br><span class="line"><span class="built_in">export</span> JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$JAVA_HOME</span>/bin:<span class="variable">$PATH</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Verify</span></span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$JAVA_HOME</span></span><br><span class="line">java -version</span><br></pre></td></tr></table></figure><h3 id="Windows-Command-Prompt"><a href="#Windows-Command-Prompt" class="headerlink" title="Windows (Command Prompt)"></a>Windows (Command Prompt)</h3><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"># <span class="built_in">Set</span> JAVA_HOME permanently (run as Admin)</span><br><span class="line">setx JAVA_HOME &quot;C:\Program Files\Java\jdk-<span class="number">17</span>&quot; /M</span><br><span class="line">setx <span class="built_in">PATH</span> &quot;<span class="variable">%PATH%</span>;<span class="variable">%JAVA_HOME%</span>\bin&quot; /M</span><br><span class="line"></span><br><span class="line"># <span class="built_in">Verify</span> (restart <span class="built_in">CMD</span> first)</span><br><span class="line"><span class="built_in">echo</span> <span class="variable">%JAVA_HOME%</span></span><br><span class="line">java -version</span><br></pre></td></tr></table></figure><h3 id="macOS-with-Android-Studio-JDK"><a href="#macOS-with-Android-Studio-JDK" class="headerlink" title="macOS with Android Studio JDK"></a>macOS with Android Studio JDK</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Add to ~/.zshrc</span></span><br><span class="line"><span class="built_in">export</span> JAVA_HOME=<span class="string">&quot;/Applications/Android Studio.app/Contents/jbr/Contents/Home&quot;</span></span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$JAVA_HOME</span>/bin:<span class="variable">$PATH</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Reload</span></span><br><span class="line"><span class="built_in">source</span> ~/.zshrc</span><br></pre></td></tr></table></figure><h2 id="Additional-Environment-Variables"><a href="#Additional-Environment-Variables" class="headerlink" title="Additional Environment Variables"></a>Additional Environment Variables</h2><p>If you’re also developing Android apps, you might need:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Android SDK</span></span><br><span class="line"><span class="built_in">export</span> ANDROID_HOME=<span class="variable">$HOME</span>/Library/Android/sdk</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PATH</span>:<span class="variable">$ANDROID_HOME</span>/tools:<span class="variable">$ANDROID_HOME</span>/platform-tools</span><br><span class="line"></span><br><span class="line"><span class="comment"># Or on Linux</span></span><br><span class="line"><span class="built_in">export</span> ANDROID_HOME=<span class="variable">$HOME</span>/Android/Sdk</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PATH</span>:<span class="variable">$ANDROID_HOME</span>/tools:<span class="variable">$ANDROID_HOME</span>/platform-tools</span><br></pre></td></tr></table></figure><p>For Node.js version management with NVM:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Add to ~/.bashrc or ~/.zshrc</span></span><br><span class="line"><span class="built_in">export</span> NVM_DIR=<span class="string">&quot;<span class="variable">$HOME</span>/.nvm&quot;</span></span><br><span class="line">[ -s <span class="string">&quot;<span class="variable">$NVM_DIR</span>/nvm.sh&quot;</span> ] &amp;&amp; \. <span class="string">&quot;<span class="variable">$NVM_DIR</span>/nvm.sh&quot;</span></span><br><span class="line">[ -s <span class="string">&quot;<span class="variable">$NVM_DIR</span>/bash_completion&quot;</span> ] &amp;&amp; \. <span class="string">&quot;<span class="variable">$NVM_DIR</span>/bash_completion&quot;</span></span><br></pre></td></tr></table></figure><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>Setting JAVA_HOME correctly ensures your Java development tools work smoothly. Remember to:</p><ul><li>Use the JDK directory, not JRE</li><li>Add <code>$JAVA_HOME/bin</code> to your PATH</li><li>Reload your shell after making changes</li><li>Verify with <code>java -version</code> and <code>echo $JAVA_HOME</code></li></ul><p>For projects requiring specific Java versions, consider using version managers like jenv (macOS&#x2F;Linux) or SDKMAN! (all platforms).</p>]]></content>
    
    
    <summary type="html">Quick guide to setting JAVA_HOME environment variable on Linux, macOS, and Windows</summary>
    
    
    
    <category term="Linux" scheme="https://devapro.github.io/categories/Linux/"/>
    
    
    <category term="linux" scheme="https://devapro.github.io/tags/linux/"/>
    
    <category term="java" scheme="https://devapro.github.io/tags/java/"/>
    
    <category term="macOS" scheme="https://devapro.github.io/tags/macOS/"/>
    
  </entry>
  
  <entry>
    <title>How to Configure Swap File on Raspberry Pi</title>
    <link href="https://devapro.github.io/en/2025/12/29/swap/"/>
    <id>https://devapro.github.io/en/2025/12/29/swap/</id>
    <published>2025-12-29T16:30:00.000Z</published>
    <updated>2026-06-28T15:21:53.870Z</updated>
    
    <content type="html"><![CDATA[<p>Swap space is essential for Raspberry Pi systems with limited RAM. This guide shows you how to create and configure a swap file to improve system stability and performance.</p><h2 id="What-is-Swap"><a href="#What-is-Swap" class="headerlink" title="What is Swap?"></a>What is Swap?</h2><p>Swap is disk space used as virtual memory when your system’s RAM is full. When RAM usage is high, Linux moves inactive pages from RAM to swap, freeing up memory for active processes.</p><h3 id="Why-You-Need-Swap-on-Raspberry-Pi"><a href="#Why-You-Need-Swap-on-Raspberry-Pi" class="headerlink" title="Why You Need Swap on Raspberry Pi"></a>Why You Need Swap on Raspberry Pi</h3><ul><li><strong>Prevent Out-of-Memory (OOM) Errors</strong>: Protects against crashes when RAM is exhausted</li><li><strong>Run Memory-Intensive Applications</strong>: Allows compilation, image processing, or databases</li><li><strong>Improve Multitasking</strong>: System remains responsive under memory pressure</li><li><strong>Enable Hibernation</strong>: Required for suspend-to-disk (if configured)</li></ul><p><strong>Note</strong>: While swap helps, it’s much slower than RAM. Don’t rely on it for performance-critical operations.</p><h2 id="Prerequisites"><a href="#Prerequisites" class="headerlink" title="Prerequisites"></a>Prerequisites</h2><ul><li>Raspberry Pi with Raspbian&#x2F;Raspberry Pi OS</li><li>Root or sudo access</li><li>Free disk space (recommended: 1-2GB for swap)</li><li>SD card with wear leveling (swap causes more writes)</li></ul><h2 id="Step-1-Check-Current-Swap-Status"><a href="#Step-1-Check-Current-Swap-Status" class="headerlink" title="Step 1: Check Current Swap Status"></a>Step 1: Check Current Swap Status</h2><p>First, verify if swap is already configured:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> swapon --show</span><br></pre></td></tr></table></figure><p><strong>Expected output:</strong></p><ul><li>If swap exists: Shows swap file&#x2F;partition details (name, size, usage)</li><li>If no swap: No output (empty)</li></ul><p>You can also check with:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">free -h</span><br></pre></td></tr></table></figure><p>Look at the “Swap” row. If it shows 0B, no swap is configured.</p><h2 id="Step-2-Create-Swap-File"><a href="#Step-2-Create-Swap-File" class="headerlink" title="Step 2: Create Swap File"></a>Step 2: Create Swap File</h2><p>Create a 1GB swap file using <code>fallocate</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> fallocate -l 1G /swapfile</span><br></pre></td></tr></table></figure><p><strong>Command breakdown:</strong></p><ul><li><code>fallocate</code>: Quickly allocates disk space</li><li><code>-l 1G</code>: Size of the swap file (1 gigabyte)</li><li><code>/swapfile</code>: Location and name of the swap file</li></ul><p><strong>Choosing swap size:</strong></p><ul><li><strong>512MB RAM or less</strong>: 1-2GB swap</li><li><strong>1GB RAM</strong>: 1GB swap</li><li><strong>2GB RAM</strong>: 512MB-1GB swap</li><li><strong>4GB+ RAM</strong>: 512MB or none</li></ul><p><strong>Alternative method</strong> (if fallocate doesn’t work):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/zero of=/swapfile bs=1M count=1024</span><br></pre></td></tr></table></figure><p>This creates a 1GB file (1024 blocks × 1MB each).</p><h2 id="Step-3-Verify-File-Creation"><a href="#Step-3-Verify-File-Creation" class="headerlink" title="Step 3: Verify File Creation"></a>Step 3: Verify File Creation</h2><p>Check that the swap file was created with the correct size:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> -lh /swapfile</span><br></pre></td></tr></table></figure><p><strong>Expected output:</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-rw-r--r-- 1 root root 1.0G Dec 29 17:30 /swapfile</span><br></pre></td></tr></table></figure><p>The file should be exactly 1GB.</p><h2 id="Step-4-Set-Correct-Permissions"><a href="#Step-4-Set-Correct-Permissions" class="headerlink" title="Step 4: Set Correct Permissions"></a>Step 4: Set Correct Permissions</h2><p><strong>Critical security step!</strong> Swap files must be readable only by root to prevent information leaks:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 600 /swapfile</span><br></pre></td></tr></table></figure><p>Verify the permissions changed:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> -lh /swapfile</span><br></pre></td></tr></table></figure><p><strong>Expected output:</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-rw------- 1 root root 1.0G Dec 29 17:30 /swapfile</span><br></pre></td></tr></table></figure><p>Notice the permissions are now <code>600</code> (only root can read&#x2F;write).</p><p><strong>Why this matters:</strong> Swap may contain sensitive data from memory (passwords, encryption keys). Wrong permissions could expose this data to other users.</p><h2 id="Step-5-Format-as-Swap"><a href="#Step-5-Format-as-Swap" class="headerlink" title="Step 5: Format as Swap"></a>Step 5: Format as Swap</h2><p>Set up the file as swap space:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> mkswap /swapfile</span><br></pre></td></tr></table></figure><p><strong>Expected output:</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Setting up swapspace version 1, size = 1024 MiB (1073737728 bytes)</span><br><span class="line">no label, UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</span><br></pre></td></tr></table></figure><p>This formats the file with the swap signature and metadata.</p><h2 id="Step-6-Enable-Swap"><a href="#Step-6-Enable-Swap" class="headerlink" title="Step 6: Enable Swap"></a>Step 6: Enable Swap</h2><p>Activate the swap file:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> swapon /swapfile</span><br></pre></td></tr></table></figure><p>The swap is now active! Verify it:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> swapon --show</span><br></pre></td></tr></table></figure><p><strong>Expected output:</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">NAME      TYPE SIZE USED PRIO</span><br><span class="line">/swapfile file   1G   0B   -2</span><br></pre></td></tr></table></figure><p>Or check with:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">free -h</span><br></pre></td></tr></table></figure><p>You should see swap space listed under the “Swap” row.</p><h2 id="Step-7-Make-Swap-Permanent"><a href="#Step-7-Make-Swap-Permanent" class="headerlink" title="Step 7: Make Swap Permanent"></a>Step 7: Make Swap Permanent</h2><p>The swap file will be lost on reboot unless you add it to <code>/etc/fstab</code>.</p><p>Back up fstab first:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">cp</span> /etc/fstab /etc/fstab.backup</span><br></pre></td></tr></table></figure><p>Add the swap file to fstab:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;/swapfile none swap sw 0 0&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/fstab</span><br></pre></td></tr></table></figure><p>Verify it was added:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">tail</span> -1 /etc/fstab</span><br></pre></td></tr></table></figure><p><strong>Expected output:</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/swapfile none swap sw 0 0</span><br></pre></td></tr></table></figure><p>Now swap will automatically activate on boot.</p><h2 id="Step-8-Adjust-Swappiness-Optional"><a href="#Step-8-Adjust-Swappiness-Optional" class="headerlink" title="Step 8: Adjust Swappiness (Optional)"></a>Step 8: Adjust Swappiness (Optional)</h2><p>Swappiness controls how aggressively the kernel uses swap. Value range: 0-100.</p><p>Check current swappiness:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> /proc/sys/vm/swappiness</span><br></pre></td></tr></table></figure><p><strong>Default</strong>: Usually 60</p><p><strong>Recommended for Raspberry Pi:</strong></p><ul><li><strong>10</strong>: Prefer RAM, use swap only when necessary (recommended for SD cards)</li><li><strong>60</strong>: Default balanced behavior</li><li><strong>100</strong>: Aggressively use swap</li></ul><p>Set swappiness temporarily (lost on reboot):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> sysctl vm.swappiness=10</span><br></pre></td></tr></table></figure><p>Make it permanent:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;vm.swappiness=10&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/sysctl.conf</span><br></pre></td></tr></table></figure><p><strong>Why lower swappiness on Pi:</strong></p><ul><li>Reduces SD card wear (fewer write cycles)</li><li>Improves performance (RAM is much faster)</li><li>Only uses swap when truly needed</li></ul><h2 id="Step-9-Adjust-Cache-Pressure-Optional"><a href="#Step-9-Adjust-Cache-Pressure-Optional" class="headerlink" title="Step 9: Adjust Cache Pressure (Optional)"></a>Step 9: Adjust Cache Pressure (Optional)</h2><p>Cache pressure determines how aggressively the kernel reclaims inode and dentry caches.</p><p>Check current value:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> /proc/sys/vm/vfs_cache_pressure</span><br></pre></td></tr></table></figure><p><strong>Default</strong>: 100</p><p>Set a lower value to keep more file system cache in memory:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> sysctl vm.vfs_cache_pressure=50</span><br></pre></td></tr></table></figure><p>Make it permanent:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;vm.vfs_cache_pressure=50&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/sysctl.conf</span><br></pre></td></tr></table></figure><h2 id="Verification"><a href="#Verification" class="headerlink" title="Verification"></a>Verification</h2><p>Test that everything is working correctly:</p><h3 id="Check-Swap-Status"><a href="#Check-Swap-Status" class="headerlink" title="Check Swap Status"></a>Check Swap Status</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Show swap devices</span></span><br><span class="line"><span class="built_in">sudo</span> swapon --show</span><br><span class="line"></span><br><span class="line"><span class="comment"># Show memory and swap usage</span></span><br><span class="line">free -h</span><br><span class="line"></span><br><span class="line"><span class="comment"># Show detailed memory info</span></span><br><span class="line"><span class="built_in">cat</span> /proc/meminfo | grep -i swap</span><br></pre></td></tr></table></figure><h3 id="Test-Swap-Under-Load"><a href="#Test-Swap-Under-Load" class="headerlink" title="Test Swap Under Load"></a>Test Swap Under Load</h3><p>Create artificial memory pressure to force swap usage:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Install stress tool</span></span><br><span class="line"><span class="built_in">sudo</span> apt-get install stress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Run memory stress test (use 90% of RAM)</span></span><br><span class="line">stress --vm 1 --vm-bytes $(awk <span class="string">&#x27;/MemAvailable/&#123;printf &quot;%d\n&quot;, $2 * 0.9;&#125;&#x27;</span> &lt; /proc/meminfo)k --<span class="built_in">timeout</span> 30s</span><br></pre></td></tr></table></figure><p>Monitor swap usage during the test:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">watch -n 1 free -h</span><br></pre></td></tr></table></figure><p>You should see the “Swap” usage increase.</p><h2 id="Troubleshooting"><a href="#Troubleshooting" class="headerlink" title="Troubleshooting"></a>Troubleshooting</h2><h3 id="Swap-Not-Activating-on-Boot"><a href="#Swap-Not-Activating-on-Boot" class="headerlink" title="Swap Not Activating on Boot"></a>Swap Not Activating on Boot</h3><p><strong>Check fstab entry:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> /etc/fstab | grep swap</span><br></pre></td></tr></table></figure><p><strong>Test fstab manually:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> swapoff /swapfile</span><br><span class="line"><span class="built_in">sudo</span> swapon -a  <span class="comment"># Activates all swap in fstab</span></span><br><span class="line"><span class="built_in">sudo</span> swapon --show</span><br></pre></td></tr></table></figure><h3 id="“fallocate-fallocate-failed-Operation-not-supported”"><a href="#“fallocate-fallocate-failed-Operation-not-supported”" class="headerlink" title="“fallocate: fallocate failed: Operation not supported”"></a>“fallocate: fallocate failed: Operation not supported”</h3><p>Some filesystems don’t support fallocate. Use <code>dd</code> instead:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/zero of=/swapfile bs=1M count=1024 status=progress</span><br></pre></td></tr></table></figure><h3 id="Swap-File-Too-Large-for-SD-Card"><a href="#Swap-File-Too-Large-for-SD-Card" class="headerlink" title="Swap File Too Large for SD Card"></a>Swap File Too Large for SD Card</h3><p>Reduce swap size:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Remove existing swap</span></span><br><span class="line"><span class="built_in">sudo</span> swapoff /swapfile</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">rm</span> /swapfile</span><br><span class="line"></span><br><span class="line"><span class="comment"># Create smaller swap (512MB)</span></span><br><span class="line"><span class="built_in">sudo</span> fallocate -l 512M /swapfile</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 600 /swapfile</span><br><span class="line"><span class="built_in">sudo</span> mkswap /swapfile</span><br><span class="line"><span class="built_in">sudo</span> swapon /swapfile</span><br></pre></td></tr></table></figure><h3 id="System-Still-Running-Out-of-Memory"><a href="#System-Still-Running-Out-of-Memory" class="headerlink" title="System Still Running Out of Memory"></a>System Still Running Out of Memory</h3><ul><li><strong>Increase swap size</strong>: Follow removal steps, then create larger swap</li><li><strong>Reduce swappiness</strong>: Set to 10 or lower</li><li><strong>Close unnecessary services</strong>: Free up RAM</li><li><strong>Add more physical RAM</strong>: Consider upgrading Pi model</li></ul><h3 id="SD-Card-Wearing-Out-Quickly"><a href="#SD-Card-Wearing-Out-Quickly" class="headerlink" title="SD Card Wearing Out Quickly"></a>SD Card Wearing Out Quickly</h3><p>Swap causes many write cycles. To reduce wear:</p><ol><li><strong>Lower swappiness to 10</strong></li><li><strong>Use USB storage for swap</strong> instead of SD card:<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> fallocate -l 1G /media/usb/swapfile</span><br><span class="line"><span class="comment"># Follow same steps but use USB path</span></span><br></pre></td></tr></table></figure></li><li><strong>Monitor SD card health</strong> regularly</li></ol><h2 id="Managing-Swap"><a href="#Managing-Swap" class="headerlink" title="Managing Swap"></a>Managing Swap</h2><h3 id="Disable-Swap-Temporarily"><a href="#Disable-Swap-Temporarily" class="headerlink" title="Disable Swap Temporarily"></a>Disable Swap Temporarily</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> swapoff /swapfile</span><br></pre></td></tr></table></figure><h3 id="Re-enable-Swap"><a href="#Re-enable-Swap" class="headerlink" title="Re-enable Swap"></a>Re-enable Swap</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> swapon /swapfile</span><br></pre></td></tr></table></figure><h3 id="Remove-Swap-Completely"><a href="#Remove-Swap-Completely" class="headerlink" title="Remove Swap Completely"></a>Remove Swap Completely</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Disable swap</span></span><br><span class="line"><span class="built_in">sudo</span> swapoff /swapfile</span><br><span class="line"></span><br><span class="line"><span class="comment"># Remove fstab entry</span></span><br><span class="line"><span class="built_in">sudo</span> sed -i <span class="string">&#x27;/\/swapfile/d&#x27;</span> /etc/fstab</span><br><span class="line"></span><br><span class="line"><span class="comment"># Delete swap file</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">rm</span> /swapfile</span><br></pre></td></tr></table></figure><h3 id="Resize-Swap"><a href="#Resize-Swap" class="headerlink" title="Resize Swap"></a>Resize Swap</h3><p>To change swap size, you must remove and recreate it:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Disable current swap</span></span><br><span class="line"><span class="built_in">sudo</span> swapoff /swapfile</span><br><span class="line"></span><br><span class="line"><span class="comment"># Remove old swap file</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">rm</span> /swapfile</span><br><span class="line"></span><br><span class="line"><span class="comment"># Create new swap with desired size (e.g., 2GB)</span></span><br><span class="line"><span class="built_in">sudo</span> fallocate -l 2G /swapfile</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 600 /swapfile</span><br><span class="line"><span class="built_in">sudo</span> mkswap /swapfile</span><br><span class="line"><span class="built_in">sudo</span> swapon /swapfile</span><br><span class="line"></span><br><span class="line"><span class="comment"># Verify</span></span><br><span class="line"><span class="built_in">sudo</span> swapon --show</span><br></pre></td></tr></table></figure><h2 id="Performance-Monitoring"><a href="#Performance-Monitoring" class="headerlink" title="Performance Monitoring"></a>Performance Monitoring</h2><p>Monitor swap usage over time:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Real-time monitoring</span></span><br><span class="line">watch -n 2 <span class="string">&#x27;free -h &amp;&amp; echo &amp;&amp; swapon --show&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Check swap I/O</span></span><br><span class="line">iostat -x 2</span><br><span class="line"></span><br><span class="line"><span class="comment"># View processes using swap</span></span><br><span class="line"><span class="keyword">for</span> file <span class="keyword">in</span> /proc/*/status ; <span class="keyword">do</span> awk <span class="string">&#x27;/VmSwap|Name/&#123;printf $2 &quot; &quot; $3&#125;END&#123; print &quot;&quot;&#125;&#x27;</span> <span class="variable">$file</span>; <span class="keyword">done</span> | <span class="built_in">sort</span> -k 2 -n -r | <span class="built_in">head</span> -10</span><br></pre></td></tr></table></figure><h2 id="Alternative-Using-zram-Instead"><a href="#Alternative-Using-zram-Instead" class="headerlink" title="Alternative: Using zram Instead"></a>Alternative: Using zram Instead</h2><p>For better performance and less SD card wear, consider zram (compressed RAM):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Install zram tools</span></span><br><span class="line"><span class="built_in">sudo</span> apt install zram-tools</span><br><span class="line"></span><br><span class="line"><span class="comment"># Configure zram</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;ALGO=lz4&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/default/zramswap</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;PERCENT=50&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/default/zramswap</span><br><span class="line"></span><br><span class="line"><span class="comment"># Start zram</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl start zramswap</span><br><span class="line"></span><br><span class="line"><span class="comment"># Enable on boot</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> zramswap</span><br></pre></td></tr></table></figure><p>Zram compresses memory in RAM itself, avoiding slow disk I&#x2F;O and SD card wear.</p><h2 id="Quick-Reference"><a href="#Quick-Reference" class="headerlink" title="Quick Reference"></a>Quick Reference</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Check swap status</span></span><br><span class="line"><span class="built_in">sudo</span> swapon --show</span><br><span class="line">free -h</span><br><span class="line"></span><br><span class="line"><span class="comment"># Create 1GB swap</span></span><br><span class="line"><span class="built_in">sudo</span> fallocate -l 1G /swapfile</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 600 /swapfile</span><br><span class="line"><span class="built_in">sudo</span> mkswap /swapfile</span><br><span class="line"><span class="built_in">sudo</span> swapon /swapfile</span><br><span class="line"></span><br><span class="line"><span class="comment"># Make permanent</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;/swapfile none swap sw 0 0&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/fstab</span><br><span class="line"></span><br><span class="line"><span class="comment"># Optimize for SD card</span></span><br><span class="line"><span class="built_in">sudo</span> sysctl vm.swappiness=10</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;vm.swappiness=10&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/sysctl.conf</span><br><span class="line"></span><br><span class="line"><span class="comment"># Disable swap</span></span><br><span class="line"><span class="built_in">sudo</span> swapoff /swapfile</span><br><span class="line"></span><br><span class="line"><span class="comment"># Enable swap</span></span><br><span class="line"><span class="built_in">sudo</span> swapon /swapfile</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">Guide to creating and configuring a swap file on Raspberry Pi to improve performance and prevent out-of-memory errors</summary>
    
    
    
    <category term="Linux" scheme="https://devapro.github.io/categories/Linux/"/>
    
    
    <category term="linux" scheme="https://devapro.github.io/tags/linux/"/>
    
    <category term="raspberry pi" scheme="https://devapro.github.io/tags/raspberry-pi/"/>
    
    <category term="swap" scheme="https://devapro.github.io/tags/swap/"/>
    
  </entry>
  
  <entry>
    <title>Self-hosted SIP server</title>
    <link href="https://devapro.github.io/en/2025/12/12/asterisk/"/>
    <id>https://devapro.github.io/en/2025/12/12/asterisk/</id>
    <published>2025-12-12T22:14:34.000Z</published>
    <updated>2026-06-28T15:21:53.868Z</updated>
    
    <content type="html"><![CDATA[<p>If you need to connect a few SIP devices in a local network, or via the internet without using a commercial SIP server, you can do it on a small VPS, Raspberry Pi.<br>(I suppose you already have VPS or Raspberry Pi and know how to use command line)</p><h3 id="Step-1-Install"><a href="#Step-1-Install" class="headerlink" title="Step 1. Install"></a>Step 1. Install</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install asterisk</span><br></pre></td></tr></table></figure><p>Check that asterisk started (you should see “active (running)”)</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl status asterisk</span><br></pre></td></tr></table></figure><h3 id="Step-2-Configure-accounts"><a href="#Step-2-Configure-accounts" class="headerlink" title="Step 2. Configure accounts"></a>Step 2. Configure accounts</h3><p>In <code>sudo nano /etc/asterisk/pjsip.conf</code></p><p>Add configuration:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="section">[transport-udp]</span></span><br><span class="line"><span class="attr">type</span>=transport</span><br><span class="line"><span class="attr">protocol</span>=udp </span><br><span class="line"><span class="attr">bind</span>=<span class="number">0.0</span>.<span class="number">0.0</span>:<span class="number">5060</span> <span class="comment">; you can change default port</span></span><br><span class="line"><span class="attr">external_media_address</span>=[ip address of VPS]</span><br><span class="line"><span class="attr">external_signaling_address</span>=[ip address of VPS]</span><br><span class="line"><span class="attr">local_net</span>=<span class="number">192.168</span>.<span class="number">0.0</span>/<span class="number">16</span> <span class="comment">;optional</span></span><br><span class="line"><span class="attr">local_net</span>=<span class="number">10.0</span>.<span class="number">0.0</span>/<span class="number">8</span> <span class="comment">;optional</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; ----- User 1 -----</span></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=endpoint</span><br><span class="line"><span class="attr">context</span>=internal</span><br><span class="line"><span class="attr">disallow</span>=all</span><br><span class="line"><span class="attr">allow</span>=ulaw</span><br><span class="line"><span class="attr">auth</span>=<span class="number">1001</span></span><br><span class="line"><span class="attr">aors</span>=<span class="number">1001</span></span><br><span class="line"><span class="attr">direct_media</span>=<span class="literal">no</span> <span class="comment">; Force RTP through Asterisk </span></span><br><span class="line"><span class="attr">rtp_symmetric</span>=<span class="literal">yes</span> <span class="comment">; Important for NAT </span></span><br><span class="line"><span class="attr">force_rport</span>=<span class="literal">yes</span> <span class="comment">; Important for NAT </span></span><br><span class="line"><span class="attr">rewrite_contact</span>=<span class="literal">yes</span> <span class="comment">; Important for NAT</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=auth</span><br><span class="line"><span class="attr">auth_type</span>=userpass</span><br><span class="line"><span class="attr">password</span>=strongpassword1</span><br><span class="line"><span class="attr">username</span>=<span class="number">1001</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=aor</span><br><span class="line"><span class="attr">max_contacts</span>=<span class="number">1</span></span><br><span class="line"><span class="attr">remove_existing</span>=<span class="literal">yes</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; ----- User 2 -----</span></span><br><span class="line"><span class="section">[1002]</span></span><br><span class="line"><span class="attr">type</span>=endpoint</span><br><span class="line"><span class="attr">context</span>=internal</span><br><span class="line"><span class="attr">disallow</span>=all</span><br><span class="line"><span class="attr">allow</span>=ulaw</span><br><span class="line"><span class="attr">auth</span>=<span class="number">1002</span></span><br><span class="line"><span class="attr">aors</span>=<span class="number">1002</span></span><br><span class="line"><span class="attr">direct_media</span>=<span class="literal">no</span> <span class="comment">; Force RTP through Asterisk </span></span><br><span class="line"><span class="attr">rtp_symmetric</span>=<span class="literal">yes</span> <span class="comment">; Important for NAT </span></span><br><span class="line"><span class="attr">force_rport</span>=<span class="literal">yes</span> <span class="comment">; Important for NAT </span></span><br><span class="line"><span class="attr">rewrite_contact</span>=<span class="literal">yes</span> <span class="comment">; Important for NAT</span></span><br><span class="line"><span class="attr">media_encryption</span>=sdes  <span class="comment">; Enable SRTP for encrypted audio, which can be not supported on old devices, set no for old devices or apps. Or sdes, or dtls, or srtp</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1002]</span></span><br><span class="line"><span class="attr">type</span>=auth</span><br><span class="line"><span class="attr">auth_type</span>=userpass</span><br><span class="line"><span class="attr">password</span>=strongpassword2</span><br><span class="line"><span class="attr">username</span>=<span class="number">1002</span></span><br><span class="line"><span class="comment">;realm=[your domain] ; in some cases if you set defaul-realm you need to set the same for all clients</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1002]</span></span><br><span class="line"><span class="attr">type</span>=aor</span><br><span class="line"><span class="attr">max_contacts</span>=<span class="number">1</span></span><br><span class="line"><span class="attr">remove_existing</span>=<span class="literal">yes</span></span><br><span class="line"><span class="attr">qualify_frequency</span>=<span class="number">60</span></span><br><span class="line"><span class="attr">maximum_expiration</span>=<span class="number">3600</span></span><br><span class="line"><span class="attr">minimum_expiration</span>=<span class="number">60</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; Repeat if you need more users</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>Explanation of key parameters:</strong></p><ul><li><code>type=endpoint</code> - Defines the SIP endpoint</li><li><code>context=internal</code> - Which dialplan context to use</li><li><code>auth=1001</code> - Links to the auth section</li><li><code>aors=1001</code> - Links to the AOR (Address of Record)</li><li><code>max_contacts=1</code> - How many devices can register simultaneously</li><li><code>remove_existing=yes</code> - Replaces old registration on new login</li><li><code>direct_media=no</code> - Forces RTP through Asterisk (useful for NAT)</li><li><code>media_encryption=no</code> - Encryption</li></ul><p><strong>Options media_encryption:</strong></p><ul><li><code>no</code> - No encryption</li><li><code>sdes</code> - SRTP with SDES key exchange</li><li><code>dtls</code> - SRTP with DTLS key exchange<br>Use Optional Encryption:</li></ul><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">media_encryption</span>=sdes</span><br><span class="line"><span class="attr">media_encryption_optimistic</span>=<span class="literal">yes</span> <span class="comment">; Allow fallback to unencrypted</span></span><br></pre></td></tr></table></figure><p>Define Dialplan in <code>/etc/asterisk/extensions.conf</code><br>(For example, any user can call another (e.g., 1001 → 1002).)</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="section">[internal]</span></span><br><span class="line"><span class="attr">exten</span> =&gt; _1XXX,<span class="number">1</span>,Dial(PJSIP/<span class="variable">$&#123;EXTEN&#125;</span>,<span class="number">20</span>)</span><br><span class="line"><span class="attr">exten</span> =&gt; _1XXX,n,Hangup()</span><br></pre></td></tr></table></figure><p>Reload Asterisk</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> asterisk -rx <span class="string">&quot;reload&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># OR</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> systemctl restart asterisk</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>To verify users</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> asterisk -rvvv</span><br><span class="line"><span class="comment"># then:</span></span><br><span class="line">pjsip show endpoints</span><br></pre></td></tr></table></figure><p>RTP Media Ports (Optional)<br>You can reduce numbers of ports for RTP<br>in <code>/etc/asterisk/rtp.conf</code></p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[general]</span></span><br><span class="line"><span class="attr">rtpstart</span>=<span class="number">10000</span></span><br><span class="line"><span class="attr">rtpend</span>=<span class="number">20000</span></span><br></pre></td></tr></table></figure><p>Check logs</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> asterisk -rvvv</span><br></pre></td></tr></table></figure><p>On my old Ubuntu server, I needed additional changes for using PJSIP instead of <strong>chan_sip</strong> (the legacy SIP channel driver). Without these changes, settings from above will not work (because settings for chan_sip should be placed in <code>/etc/asterisk/sip.conf</code> instead).</p><p>Config of Load PJSIP Module<br>in <code>/etc/asterisk/modules.conf</code> update list of modules for loading:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[modules]</span></span><br><span class="line"><span class="attr">autoload</span>=<span class="literal">yes</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; Disable chan_sip</span></span><br><span class="line"><span class="attr">noload</span> =&gt; chan_sip.so</span><br><span class="line"></span><br><span class="line"><span class="comment">; Ensure PJSIP modules are loaded</span></span><br><span class="line"><span class="attr">load</span> =&gt; res_pjsip.so</span><br><span class="line"><span class="attr">load</span> =&gt; res_pjsip_session.so</span><br><span class="line"><span class="attr">load</span> =&gt; res_pjsip_registrar.so</span><br><span class="line"><span class="attr">load</span> =&gt; res_pjsip_outbound_registration.so</span><br><span class="line"><span class="attr">load</span> =&gt; chan_pjsip.so</span><br></pre></td></tr></table></figure><h3 id="STUN"><a href="#STUN" class="headerlink" title="STUN"></a>STUN</h3><p>In file:  <code>/etc/asterisk/rtp.conf</code></p><p>add:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[general]</span></span><br><span class="line"><span class="attr">rtpstart</span>=<span class="number">10000</span></span><br><span class="line"><span class="attr">rtpend</span>=<span class="number">20000</span></span><br><span class="line"><span class="attr">stunaddr</span>=stun.l.google.com:<span class="number">19302</span>  <span class="comment">; For NAT traversal</span></span><br></pre></td></tr></table></figure><p>Check clients:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">asterisk -rx <span class="string">&quot;pjsip show endpoints&quot;</span> </span><br><span class="line">asterisk -rx <span class="string">&quot;pjsip show contacts&quot;</span></span><br></pre></td></tr></table></figure><h3 id="Debug-Media-Negotiation-Issues"><a href="#Debug-Media-Negotiation-Issues" class="headerlink" title="Debug Media Negotiation Issues"></a>Debug Media Negotiation Issues</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">asterisk -rvvvvv</span><br></pre></td></tr></table></figure><p>Then in the CLI:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pjsip set logger on</span><br><span class="line">core set debug 5</span><br></pre></td></tr></table></figure><p>Make a Test Call and Watch for:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">-- Called PJSIP/1002</span><br><span class="line">-- PJSIP/1002-00000001 is ringing</span><br><span class="line">[NOTICE] res_pjsip_session.c: Incompatible media format - no common codec</span><br></pre></td></tr></table></figure><p>Or:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">WARNING[xxxxx]: res_pjsip_sdp_rtp.c: No common codecs between endpoints</span><br></pre></td></tr></table></figure><h4 id="Understanding-Status-Fields"><a href="#Understanding-Status-Fields" class="headerlink" title="Understanding Status Fields"></a>Understanding Status Fields</h4><ul><li><strong>Avail</strong> - Device is reachable (qualify successful)</li><li><strong>Unavail</strong> - Device didn’t respond to qualify ping, <strong>but calls may still work</strong></li><li><strong>NonQual</strong> - Qualify not configured</li><li><strong>Unknown</strong> - No qualify checks performed yet</li></ul><p><strong>Important:</strong> Status only affects call routing decisions if you use <code>qualify</code> in dialplan logic. For basic calling, it doesn’t matter!</p><h4 id="PJSIP-Security-Settings"><a href="#PJSIP-Security-Settings" class="headerlink" title="PJSIP Security Settings"></a>PJSIP Security Settings</h4><p>In <code>/etc/asterisk/pjsip.conf</code>:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="section">[global]</span></span><br><span class="line"><span class="attr">allow_anonymous</span>=<span class="literal">no</span></span><br><span class="line"><span class="attr">max_forwards</span>=<span class="number">70</span></span><br><span class="line"><span class="attr">user_agent</span>=Asterisk PBX</span><br><span class="line"><span class="attr">default_realm</span>=[your domain]</span><br><span class="line"></span><br><span class="line"><span class="comment">; ACL to restrict registration sources (optional but recommended)</span></span><br><span class="line"><span class="section">[acl]</span></span><br><span class="line"><span class="attr">type</span>=acl</span><br><span class="line"><span class="attr">deny</span>=<span class="number">0.0</span>.<span class="number">0.0</span>/<span class="number">0.0</span>.<span class="number">0.0</span></span><br><span class="line"><span class="attr">permit</span>=YOUR_<span class="literal">OFF</span>ICE_IP/<span class="number">32</span></span><br><span class="line"><span class="attr">permit</span>=YOUR_HOME_IP/<span class="number">32</span></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Disable Unnecessary Modules in <code>/etc/asterisk/modules.conf</code></p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="section">[modules]</span></span><br><span class="line"><span class="attr">autoload</span>=<span class="literal">yes</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; Disable chan_sip if using PJSIP</span></span><br><span class="line"><span class="attr">noload</span> =&gt; chan_sip.so</span><br><span class="line"></span><br><span class="line"><span class="comment">; Disable unnecessary protocols</span></span><br><span class="line"><span class="attr">noload</span> =&gt; chan_skinny.so</span><br><span class="line"><span class="attr">noload</span> =&gt; chan_mgcp.so</span><br><span class="line"><span class="attr">noload</span> =&gt; chan_unistim.so</span><br><span class="line"></span><br><span class="line"><span class="comment">; Disable web interface if not needed</span></span><br><span class="line"><span class="attr">noload</span> =&gt; res_http_websocket.so</span><br><span class="line"><span class="attr">noload</span> =&gt; res_ari.so</span><br><span class="line"><span class="attr">noload</span> =&gt; res_stasis.so</span><br></pre></td></tr></table></figure><p>Secure Asterisk Manager Interface (AMI) in <code>/etc/asterisk/manager.conf</code></p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="section">[general]</span></span><br><span class="line"><span class="attr">enabled</span> = <span class="literal">no</span>  <span class="comment">; Disable if not needed</span></span><br><span class="line"><span class="attr">port</span> = <span class="number">5038</span></span><br><span class="line"><span class="attr">bindaddr</span> = <span class="number">127.0</span>.<span class="number">0.1</span>  <span class="comment">; Only local access</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; If you need AMI, use strong credentials:</span></span><br><span class="line"><span class="section">[admin]</span></span><br><span class="line"><span class="attr">secret</span> = VeryStr0ngAMIPass123!</span><br><span class="line"><span class="attr">deny</span> = <span class="number">0.0</span>.<span class="number">0.0</span>/<span class="number">0.0</span>.<span class="number">0.0</span></span><br><span class="line"><span class="attr">permit</span> = <span class="number">127.0</span>.<span class="number">0.1</span>/<span class="number">255.255</span>.<span class="number">255.0</span></span><br><span class="line"><span class="attr">read</span> = system,call,log,verbose,command,agent,user,config</span><br><span class="line"><span class="attr">write</span> = system,call,log,verbose,command,agent,user,config</span><br></pre></td></tr></table></figure><p>Check for suspicious activity:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Check failed registration attempts</span></span><br><span class="line">asterisk -rx <span class="string">&quot;pjsip show registrations&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Monitor active channels</span></span><br><span class="line">asterisk -rx <span class="string">&quot;core show channels&quot;</span></span><br></pre></td></tr></table></figure><h2 id="SSL"><a href="#SSL" class="headerlink" title="SSL"></a>SSL</h2><p>Option A: Let’s Encrypt (Free &amp; Recommended)</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Install Certbot</span></span><br><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install certbot</span><br><span class="line"></span><br><span class="line"><span class="comment"># Get certificate</span></span><br><span class="line"><span class="built_in">sudo</span> certbot certonly --standalone -d [your domain]</span><br><span class="line"></span><br><span class="line"><span class="comment"># Certificate will be at:</span></span><br><span class="line"><span class="comment"># /etc/letsencrypt/live/[your domain]/fullchain.pem</span></span><br><span class="line"><span class="comment"># /etc/letsencrypt/live/[your domain]/privkey.pem</span></span><br></pre></td></tr></table></figure><p>Option B: Self-Signed Certificate</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">mkdir</span> -p /etc/asterisk/keys</span><br><span class="line"><span class="built_in">cd</span> /etc/asterisk/keys</span><br><span class="line"></span><br><span class="line"><span class="comment"># Generate private key and certificate</span></span><br><span class="line"><span class="built_in">sudo</span> openssl req -new -x509 -days 365 -nodes \</span><br><span class="line">  -out asterisk.pem -keyout asterisk.key \</span><br><span class="line">  -subj <span class="string">&quot;/C=RU/ST=State/L=City/O=Organization/CN=s.mdroid.ru&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Combine them</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">cat</span> asterisk.key asterisk.pem &gt; asterisk.combined.pem</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 600 asterisk.combined.pem</span><br></pre></td></tr></table></figure><p>Set Proper Permissions</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># For Let&#x27;s Encrypt certificates</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chown</span> -R asterisk:asterisk /etc/letsencrypt/</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> -R 640 /etc/letsencrypt/live/s.mdroid.ru/*.pem</span><br><span class="line"></span><br><span class="line"><span class="comment"># For self-signed certificates</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chown</span> asterisk:asterisk /etc/asterisk/keys/*</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 600 /etc/asterisk/keys/*</span><br></pre></td></tr></table></figure><h4 id="Configure"><a href="#Configure" class="headerlink" title="Configure"></a>Configure</h4><p>In file: <code>/etc/asterisk/pjsip.conf</code></p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[global]</span></span><br><span class="line"><span class="attr">max_forwards</span>=<span class="number">70</span></span><br><span class="line"><span class="attr">user_agent</span>=Asterisk PBX</span><br><span class="line"><span class="attr">default_realm</span>=[your domain or asterisk]</span><br><span class="line"></span><br><span class="line"><span class="comment">; UDP Transport (keep for backward compatibility)</span></span><br><span class="line"><span class="section">[transport-udp]</span></span><br><span class="line">....</span><br><span class="line"></span><br><span class="line"><span class="comment">; TLS Transport (secure)</span></span><br><span class="line"><span class="section">[transport-tls]</span></span><br><span class="line"><span class="attr">type</span>=transport</span><br><span class="line"><span class="attr">protocol</span>=tls</span><br><span class="line"><span class="attr">bind</span>=<span class="number">0.0</span>.<span class="number">0.0</span>:<span class="number">5061</span></span><br><span class="line"><span class="attr">cert_file</span>=/etc/letsencrypt/live/[your domain]/fullchain.pem</span><br><span class="line"><span class="attr">priv_key_file</span>=/etc/letsencrypt/live/[your domain]/privkey.pem</span><br><span class="line"><span class="comment">; OR for self-signed:</span></span><br><span class="line"><span class="comment">; cert_file=/etc/asterisk/keys/asterisk.combined.pem</span></span><br><span class="line"><span class="comment">; priv_key_file=/etc/asterisk/keys/asterisk.combined.pem</span></span><br><span class="line"><span class="comment">;cipher=ALL ; use only if you know which format is supported</span></span><br><span class="line"><span class="attr">method</span>=sslv23 <span class="comment">; or tlsv1_2 for modern devices</span></span><br><span class="line"><span class="attr">verify_server</span>=<span class="literal">no</span></span><br><span class="line"><span class="attr">verify_client</span>=<span class="literal">no</span></span><br><span class="line"><span class="attr">external_media_address</span>=[server ip]</span><br><span class="line"><span class="attr">external_signaling_address</span>=[server ip]</span><br><span class="line"></span><br><span class="line"><span class="comment">; Update endpoints to use TLS</span></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=endpoint</span><br><span class="line"><span class="attr">context</span>=internal</span><br><span class="line"><span class="comment">;transport=transport-tls ;optional to force use TLS</span></span><br><span class="line"><span class="attr">disallow</span>=all</span><br><span class="line"><span class="attr">allow</span>=ulaw</span><br><span class="line"><span class="attr">allow</span>=alaw</span><br><span class="line"><span class="attr">auth</span>=<span class="number">1001</span></span><br><span class="line"><span class="attr">aors</span>=<span class="number">1001</span></span><br><span class="line"><span class="attr">direct_media</span>=<span class="literal">no</span></span><br><span class="line"><span class="attr">rtp_symmetric</span>=<span class="literal">yes</span></span><br><span class="line"><span class="attr">force_rport</span>=<span class="literal">yes</span></span><br><span class="line"><span class="attr">rewrite_contact</span>=<span class="literal">yes</span></span><br><span class="line"><span class="attr">media_encryption</span>=sdes  <span class="comment">; Enable SRTP for encrypted audio, which can be not supported on old devices, set no for old devices or apps. Or sdes, or dtls</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=auth</span><br><span class="line"><span class="attr">auth_type</span>=userpass</span><br><span class="line"><span class="attr">password</span>=YourStrongPassword</span><br><span class="line"><span class="attr">username</span>=<span class="number">1001</span></span><br><span class="line"><span class="comment">;realm=[your domain] ; in some cases if you set defaul-realm you need to set the same for all clients</span></span><br><span class="line"></span><br><span class="line"><span class="section">[1001]</span></span><br><span class="line"><span class="attr">type</span>=aor</span><br><span class="line"><span class="attr">max_contacts</span>=<span class="number">1</span></span><br><span class="line"><span class="attr">remove_existing</span>=<span class="literal">yes</span></span><br><span class="line"><span class="attr">qualify_frequency</span>=<span class="number">60</span></span><br></pre></td></tr></table></figure><p>Reload:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Reload</span></span><br><span class="line">asterisk -rx <span class="string">&quot;core reload&quot;</span></span><br><span class="line">asterisk -rx <span class="string">&quot;pjsip reload&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Verify TLS transport is running</span></span><br><span class="line">asterisk -rx <span class="string">&quot;pjsip show transports&quot;</span></span><br></pre></td></tr></table></figure><p>You should see output like:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Transport:  &lt;TransportId........&gt;  &lt;Type&gt;  &lt;cos&gt;  &lt;tos&gt;  &lt;BindAddress.....................&gt;</span><br><span class="line">===========================================================================</span><br><span class="line">transport-udp               udp      0      0  0.0.0.0:5060</span><br><span class="line">transport-tls               tls      0      0  0.0.0.0:5061</span><br></pre></td></tr></table></figure><p>If you don’t see <code>transport-tls</code>, check logs:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">tail</span> -50 /var/log/asterisk/full | grep -i transport</span><br></pre></td></tr></table></figure><p>If you see an error:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ERROR[1069851] res_sorcery_config.c: Could not create an object of type &#x27;transport&#x27; with id &#x27;transport-tls&#x27; from configuration file &#x27;pjsip.conf&#x27;</span><br></pre></td></tr></table></figure><p>Check:</p><ul><li>That path to cert files is correct</li><li>Remove or change cipher parameter (works for me: <code>cipher=ADH-AES256-SHA,ADH-AES128-SHA</code>)</li></ul><p>For self-signed certificates you need to disable validation certificate on client or add to <code>/etc/asterisk/pjsip.conf</code>:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[transport-tls]</span></span><br><span class="line">....</span><br><span class="line"><span class="attr">require_client_cert</span>=<span class="literal">no</span></span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">Self-hosted SIP server</summary>
    
    
    
    <category term="Linux" scheme="https://devapro.github.io/categories/Linux/"/>
    
    
    <category term="linux" scheme="https://devapro.github.io/tags/linux/"/>
    
    <category term="self-hosted" scheme="https://devapro.github.io/tags/self-hosted/"/>
    
    <category term="sip" scheme="https://devapro.github.io/tags/sip/"/>
    
  </entry>
  
  <entry>
    <title>VPN Client Setup on Raspberry Pi (OpenVPN &amp; WireGuard)</title>
    <link href="https://devapro.github.io/en/2025/04/22/Open-VPN-client-on-Raspbery/"/>
    <id>https://devapro.github.io/en/2025/04/22/Open-VPN-client-on-Raspbery/</id>
    <published>2025-04-22T22:03:37.000Z</published>
    <updated>2026-06-28T15:21:53.868Z</updated>
    
    <content type="html"><![CDATA[<p>This guide covers setting up VPN clients on Raspberry Pi to create secure remote access from anywhere. You’ll learn how to configure both OpenVPN and WireGuard, including automatic reconnection and network routing.</p><h2 id="Prerequisites"><a href="#Prerequisites" class="headerlink" title="Prerequisites"></a>Prerequisites</h2><ul><li>Raspberry Pi with Raspbian&#x2F;Raspberry Pi OS installed</li><li>SSH or direct access to the Pi</li><li>VPN server configuration file (.ovpn for OpenVPN or .conf for WireGuard)</li><li>Basic command line knowledge</li></ul><h2 id="Option-1-OpenVPN-Client-Setup"><a href="#Option-1-OpenVPN-Client-Setup" class="headerlink" title="Option 1: OpenVPN Client Setup"></a>Option 1: OpenVPN Client Setup</h2><h3 id="Installation"><a href="#Installation" class="headerlink" title="Installation"></a>Installation</h3><p>Install the OpenVPN client:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt-get update</span><br><span class="line"><span class="built_in">sudo</span> apt-get install openvpn -y</span><br></pre></td></tr></table></figure><h3 id="Configuration"><a href="#Configuration" class="headerlink" title="Configuration"></a>Configuration</h3><p>Create the client configuration directory and add your VPN configuration file:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Create directory if it doesn&#x27;t exist</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">mkdir</span> -p /etc/openvpn/client</span><br><span class="line"></span><br><span class="line"><span class="comment"># Copy or create your VPN configuration file</span></span><br><span class="line"><span class="built_in">sudo</span> nano /etc/openvpn/client/client.ovpn</span><br></pre></td></tr></table></figure><p>Paste your VPN provider’s configuration into this file. Typical configuration includes server address, port, certificates, and authentication details.</p><h3 id="Start-and-Enable-Service"><a href="#Start-and-Enable-Service" class="headerlink" title="Start and Enable Service"></a>Start and Enable Service</h3><p>Start the OpenVPN client and enable it to run on boot:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Start the service</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl start openvpn-client@client.service</span><br><span class="line"></span><br><span class="line"><span class="comment"># Enable on boot</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> openvpn-client@client.service</span><br><span class="line"></span><br><span class="line"><span class="comment"># Check status</span></span><br><span class="line">systemctl status openvpn-client@client.service</span><br></pre></td></tr></table></figure><p><strong>Note:</strong> The service name <code>openvpn-client@client.service</code> corresponds to the config file <code>/etc/openvpn/client/client.ovpn</code>. If your config file has a different name (e.g., <code>myvpn.ovpn</code>), use <code>openvpn-client@myvpn.service</code>.</p><h3 id="Verify-Connection"><a href="#Verify-Connection" class="headerlink" title="Verify Connection"></a>Verify Connection</h3><p>Check your public IP to confirm the VPN is working:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl ifconfig.me</span><br></pre></td></tr></table></figure><p>The IP address should match your VPN server’s location, not your actual location.</p><h3 id="Automatic-Reconnection"><a href="#Automatic-Reconnection" class="headerlink" title="Automatic Reconnection"></a>Automatic Reconnection</h3><p>To ensure the VPN automatically reconnects if the connection drops, add these parameters to your configuration file:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> nano /etc/openvpn/client/client.ovpn</span><br></pre></td></tr></table></figure><p>Add at the end of the file:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"># Keepalive: ping every 10 seconds, restart if no response for 60 seconds</span><br><span class="line">keepalive 10 60</span><br><span class="line"></span><br><span class="line"># Use connection timer for more reliable reconnection</span><br><span class="line">ping-timer-rem</span><br></pre></td></tr></table></figure><p>Restart the service to apply changes:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart openvpn-client@client.service</span><br></pre></td></tr></table></figure><h2 id="Option-2-WireGuard-Client-Setup"><a href="#Option-2-WireGuard-Client-Setup" class="headerlink" title="Option 2: WireGuard Client Setup"></a>Option 2: WireGuard Client Setup</h2><p>WireGuard is a modern, lightweight VPN protocol with better performance and simpler configuration than OpenVPN.</p><h3 id="Installation-1"><a href="#Installation-1" class="headerlink" title="Installation"></a>Installation</h3><p>Install WireGuard:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install wireguard -y</span><br></pre></td></tr></table></figure><h3 id="Configuration-1"><a href="#Configuration-1" class="headerlink" title="Configuration"></a>Configuration</h3><p>Create and configure your WireGuard interface:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Create configuration file</span></span><br><span class="line"><span class="built_in">sudo</span> nano /etc/wireguard/wg0.conf</span><br></pre></td></tr></table></figure><p>Add your WireGuard configuration (provided by your VPN server):</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">[Interface]</span><br><span class="line">PrivateKey = YOUR_PRIVATE_KEY</span><br><span class="line">Address = 10.0.0.2/24</span><br><span class="line">DNS = 1.1.1.1</span><br><span class="line"></span><br><span class="line">[Peer]</span><br><span class="line">PublicKey = SERVER_PUBLIC_KEY</span><br><span class="line">Endpoint = vpn.example.com:51820</span><br><span class="line">AllowedIPs = 0.0.0.0/0</span><br><span class="line">PersistentKeepalive = 25</span><br></pre></td></tr></table></figure><p>Set proper permissions:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 600 /etc/wireguard/wg0.conf</span><br></pre></td></tr></table></figure><h3 id="Start-and-Enable-Service-1"><a href="#Start-and-Enable-Service-1" class="headerlink" title="Start and Enable Service"></a>Start and Enable Service</h3><p>Start WireGuard and enable it on boot:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Start the VPN</span></span><br><span class="line"><span class="built_in">sudo</span> wg-quick up wg0</span><br><span class="line"></span><br><span class="line"><span class="comment"># Enable on boot</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> wg-quick@wg0</span><br><span class="line"></span><br><span class="line"><span class="comment"># Check status</span></span><br><span class="line"><span class="built_in">sudo</span> wg show</span><br></pre></td></tr></table></figure><h3 id="Enable-IP-Forwarding-for-routing-traffic"><a href="#Enable-IP-Forwarding-for-routing-traffic" class="headerlink" title="Enable IP Forwarding (for routing traffic)"></a>Enable IP Forwarding (for routing traffic)</h3><p>If you want your Raspberry Pi to route traffic through the VPN:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Enable IP forwarding</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;net.ipv4.ip_forward=1&quot;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/sysctl.conf</span><br><span class="line"><span class="built_in">sudo</span> sysctl -p</span><br></pre></td></tr></table></figure><h3 id="Configure-iptables-for-NAT"><a href="#Configure-iptables-for-NAT" class="headerlink" title="Configure iptables for NAT"></a>Configure iptables for NAT</h3><p>Set up Network Address Translation (NAT) to route traffic through the VPN:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Allow forwarding from ethernet to VPN</span></span><br><span class="line"><span class="built_in">sudo</span> iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE</span><br><span class="line"><span class="built_in">sudo</span> iptables -A FORWARD -i wg0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT</span><br><span class="line"><span class="built_in">sudo</span> iptables -A FORWARD -i eth0 -o wg0 -j ACCEPT</span><br></pre></td></tr></table></figure><p>Make iptables rules persistent across reboots:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install iptables-persistent -y</span><br></pre></td></tr></table></figure><p>During installation, choose “Yes” to save current IPv4 and IPv6 rules.</p><p>To save rules later:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> netfilter-persistent save</span><br></pre></td></tr></table></figure><h2 id="Network-Configuration"><a href="#Network-Configuration" class="headerlink" title="Network Configuration"></a>Network Configuration</h2><h3 id="Configure-Static-IP-Address"><a href="#Configure-Static-IP-Address" class="headerlink" title="Configure Static IP Address"></a>Configure Static IP Address</h3><p>For reliable VPN routing, set a static IP address for your Raspberry Pi:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> nano /etc/dhcpcd.conf</span><br></pre></td></tr></table></figure><p>Add at the end of the file (adjust values for your network):</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"># Static IP configuration</span><br><span class="line">interface eth0</span><br><span class="line">static ip_address=192.168.1.100/24</span><br><span class="line">static routers=192.168.1.1</span><br><span class="line">static domain_name_servers=192.168.1.1 8.8.8.8</span><br><span class="line"></span><br><span class="line"># For WiFi, use wlan0 instead</span><br><span class="line"># interface wlan0</span><br><span class="line"># static ip_address=192.168.1.101/24</span><br><span class="line"># static routers=192.168.1.1</span><br><span class="line"># static domain_name_servers=192.168.1.1 8.8.8.8</span><br></pre></td></tr></table></figure><p><strong>Replace with your network values:</strong></p><ul><li><code>192.168.1.100</code> - Desired static IP for your Pi</li><li><code>192.168.1.1</code> - Your router’s IP (gateway)</li><li><code>8.8.8.8</code> - Secondary DNS server (Google DNS)</li></ul><p>Restart networking:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart dhcpcd</span><br></pre></td></tr></table></figure><h2 id="Troubleshooting"><a href="#Troubleshooting" class="headerlink" title="Troubleshooting"></a>Troubleshooting</h2><h3 id="Check-VPN-Status"><a href="#Check-VPN-Status" class="headerlink" title="Check VPN Status"></a>Check VPN Status</h3><p><strong>For OpenVPN:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl status openvpn-client@client.service</span><br><span class="line">journalctl -u openvpn-client@client.service -f</span><br></pre></td></tr></table></figure><p><strong>For WireGuard:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> wg show</span><br><span class="line">journalctl -u wg-quick@wg0 -f</span><br></pre></td></tr></table></figure><h3 id="Test-Connectivity"><a href="#Test-Connectivity" class="headerlink" title="Test Connectivity"></a>Test Connectivity</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Check if VPN interface exists</span></span><br><span class="line">ip addr show</span><br><span class="line"></span><br><span class="line"><span class="comment"># Check routing table</span></span><br><span class="line">ip route</span><br><span class="line"></span><br><span class="line"><span class="comment"># Test internet through VPN</span></span><br><span class="line">curl ifconfig.me</span><br><span class="line"></span><br><span class="line"><span class="comment"># Ping VPN server</span></span><br><span class="line">ping 10.0.0.1  <span class="comment"># Replace with your VPN server IP</span></span><br></pre></td></tr></table></figure><h3 id="Common-Issues"><a href="#Common-Issues" class="headerlink" title="Common Issues"></a>Common Issues</h3><p><strong>OpenVPN won’t start:</strong></p><ul><li>Check configuration file syntax: <code>sudo openvpn --config /etc/openvpn/client/client.ovpn</code></li><li>Verify certificates and keys are correct</li><li>Check firewall settings</li></ul><p><strong>WireGuard connection drops:</strong></p><ul><li>Add <code>PersistentKeepalive = 25</code> to [Peer] section</li><li>Check if the server endpoint is reachable</li><li>Verify firewall allows UDP traffic on WireGuard port</li></ul><p><strong>No internet after connecting:</strong></p><ul><li>Verify DNS settings in VPN config</li><li>Check if <code>AllowedIPs = 0.0.0.0/0</code> for full tunnel</li><li>Test with: <code>nslookup google.com</code></li></ul>]]></content>
    
    
    <summary type="html">Guide to setting up VPN clients on Raspberry Pi for secure remote access</summary>
    
    
    
    <category term="Linux" scheme="https://devapro.github.io/categories/Linux/"/>
    
    
    <category term="linux" scheme="https://devapro.github.io/tags/linux/"/>
    
    <category term="raspberry pi" scheme="https://devapro.github.io/tags/raspberry-pi/"/>
    
    <category term="vpn" scheme="https://devapro.github.io/tags/vpn/"/>
    
    <category term="openvpn" scheme="https://devapro.github.io/tags/openvpn/"/>
    
    <category term="wireguard" scheme="https://devapro.github.io/tags/wireguard/"/>
    
  </entry>
  
  <entry>
    <title>Настройка VPN клиента на Raspberry Pi (OpenVPN и WireGuard)</title>
    <link href="https://devapro.github.io/ru/2025/04/22/vpn-client-raspberry-pi/"/>
    <id>https://devapro.github.io/ru/2025/04/22/vpn-client-raspberry-pi/</id>
    <published>2025-04-22T22:03:37.000Z</published>
    <updated>2026-06-28T15:21:53.870Z</updated>
    
    <content type="html"><![CDATA[<p>Это руководство охватывает настройку VPN клиентов на Raspberry Pi для создания безопасного удаленного доступа из любой точки мира. Вы узнаете, как настроить OpenVPN и WireGuard, включая автоматическое переподключение и маршрутизацию сети.</p><h2 id="Требования"><a href="#Требования" class="headerlink" title="Требования"></a>Требования</h2><ul><li>Raspberry Pi с установленной Raspbian&#x2F;Raspberry Pi OS</li><li>SSH или прямой доступ к Pi</li><li>Файл конфигурации VPN сервера (.ovpn для OpenVPN или .conf для WireGuard)</li><li>Базовые знания командной строки</li></ul><h2 id="Вариант-1-Настройка-клиента-OpenVPN"><a href="#Вариант-1-Настройка-клиента-OpenVPN" class="headerlink" title="Вариант 1: Настройка клиента OpenVPN"></a>Вариант 1: Настройка клиента OpenVPN</h2><h3 id="Установка"><a href="#Установка" class="headerlink" title="Установка"></a>Установка</h3><p>Установите клиент OpenVPN:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt-get update</span><br><span class="line"><span class="built_in">sudo</span> apt-get install openvpn -y</span><br></pre></td></tr></table></figure><h3 id="Конфигурация"><a href="#Конфигурация" class="headerlink" title="Конфигурация"></a>Конфигурация</h3><p>Создайте директорию для конфигурации клиента и добавьте ваш файл конфигурации VPN:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Создайте директорию, если её не существует</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">mkdir</span> -p /etc/openvpn/client</span><br><span class="line"></span><br><span class="line"><span class="comment"># Скопируйте или создайте файл конфигурации VPN</span></span><br><span class="line"><span class="built_in">sudo</span> nano /etc/openvpn/client/client.ovpn</span><br></pre></td></tr></table></figure><p>Вставьте конфигурацию от вашего VPN провайдера в этот файл. Типичная конфигурация включает адрес сервера, порт, сертификаты и данные для аутентификации.</p><h3 id="Запуск-и-включение-сервиса"><a href="#Запуск-и-включение-сервиса" class="headerlink" title="Запуск и включение сервиса"></a>Запуск и включение сервиса</h3><p>Запустите клиент OpenVPN и включите его автозапуск при загрузке:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Запустите сервис</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl start openvpn-client@client.service</span><br><span class="line"></span><br><span class="line"><span class="comment"># Включите автозапуск при загрузке</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> openvpn-client@client.service</span><br><span class="line"></span><br><span class="line"><span class="comment"># Проверьте статус</span></span><br><span class="line">systemctl status openvpn-client@client.service</span><br></pre></td></tr></table></figure><p><strong>Примечание:</strong> Имя сервиса <code>openvpn-client@client.service</code> соответствует файлу конфигурации <code>/etc/openvpn/client/client.ovpn</code>. Если ваш файл конфигурации имеет другое имя (например, <code>myvpn.ovpn</code>), используйте <code>openvpn-client@myvpn.service</code>.</p><h3 id="Проверка-подключения"><a href="#Проверка-подключения" class="headerlink" title="Проверка подключения"></a>Проверка подключения</h3><p>Проверьте ваш публичный IP, чтобы убедиться, что VPN работает:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl ifconfig.me</span><br></pre></td></tr></table></figure><p>IP адрес должен соответствовать местоположению вашего VPN сервера, а не вашему реальному местоположению.</p><h3 id="Автоматическое-переподключение"><a href="#Автоматическое-переподключение" class="headerlink" title="Автоматическое переподключение"></a>Автоматическое переподключение</h3><p>Чтобы VPN автоматически переподключался при обрыве соединения, добавьте эти параметры в файл конфигурации:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> nano /etc/openvpn/client/client.ovpn</span><br></pre></td></tr></table></figure><p>Добавьте в конец файла:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"># Keepalive: ping каждые 10 секунд, перезапуск при отсутствии ответа 60 секунд</span><br><span class="line">keepalive 10 60</span><br><span class="line"></span><br><span class="line"># Использовать таймер соединения для более надежного переподключения</span><br><span class="line">ping-timer-rem</span><br></pre></td></tr></table></figure><p>Перезапустите сервис для применения изменений:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart openvpn-client@client.service</span><br></pre></td></tr></table></figure><h2 id="Вариант-2-Настройка-клиента-WireGuard"><a href="#Вариант-2-Настройка-клиента-WireGuard" class="headerlink" title="Вариант 2: Настройка клиента WireGuard"></a>Вариант 2: Настройка клиента WireGuard</h2><p>WireGuard — это современный, легковесный VPN протокол с лучшей производительностью и более простой конфигурацией, чем OpenVPN.</p><h3 id="Установка-1"><a href="#Установка-1" class="headerlink" title="Установка"></a>Установка</h3><p>Установите WireGuard:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install wireguard -y</span><br></pre></td></tr></table></figure><h3 id="Конфигурация-1"><a href="#Конфигурация-1" class="headerlink" title="Конфигурация"></a>Конфигурация</h3><p>Создайте и настройте интерфейс WireGuard:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Создайте файл конфигурации</span></span><br><span class="line"><span class="built_in">sudo</span> nano /etc/wireguard/wg0.conf</span><br></pre></td></tr></table></figure><p>Добавьте вашу конфигурацию WireGuard (предоставленную VPN сервером):</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">[Interface]</span><br><span class="line">PrivateKey = ВАШ_ПРИВАТНЫЙ_КЛЮЧ</span><br><span class="line">Address = 10.0.0.2/24</span><br><span class="line">DNS = 1.1.1.1</span><br><span class="line"></span><br><span class="line">[Peer]</span><br><span class="line">PublicKey = ПУБЛИЧНЫЙ_КЛЮЧ_СЕРВЕРА</span><br><span class="line">Endpoint = vpn.example.com:51820</span><br><span class="line">AllowedIPs = 0.0.0.0/0</span><br><span class="line">PersistentKeepalive = 25</span><br></pre></td></tr></table></figure><p>Установите правильные права доступа:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> 600 /etc/wireguard/wg0.conf</span><br></pre></td></tr></table></figure><h3 id="Запуск-и-включение-сервиса-1"><a href="#Запуск-и-включение-сервиса-1" class="headerlink" title="Запуск и включение сервиса"></a>Запуск и включение сервиса</h3><p>Запустите WireGuard и включите его автозапуск при загрузке:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Запустите VPN</span></span><br><span class="line"><span class="built_in">sudo</span> wg-quick up wg0</span><br><span class="line"></span><br><span class="line"><span class="comment"># Включите автозапуск при загрузке</span></span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> wg-quick@wg0</span><br><span class="line"></span><br><span class="line"><span class="comment"># Проверьте статус</span></span><br><span class="line"><span class="built_in">sudo</span> wg show</span><br></pre></td></tr></table></figure><h3 id="Включение-IP-форвардинга-для-маршрутизации-трафика"><a href="#Включение-IP-форвардинга-для-маршрутизации-трафика" class="headerlink" title="Включение IP форвардинга (для маршрутизации трафика)"></a>Включение IP форвардинга (для маршрутизации трафика)</h3><p>Если вы хотите, чтобы ваш Raspberry Pi маршрутизировал трафик через VPN:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Включите IP форвардинг</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;net.ipv4.ip_forward=1&quot;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/sysctl.conf</span><br><span class="line"><span class="built_in">sudo</span> sysctl -p</span><br></pre></td></tr></table></figure><h3 id="Настройка-iptables-для-NAT"><a href="#Настройка-iptables-для-NAT" class="headerlink" title="Настройка iptables для NAT"></a>Настройка iptables для NAT</h3><p>Настройте трансляцию сетевых адресов (NAT) для маршрутизации трафика через VPN:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Разрешите форвардинг с ethernet на VPN</span></span><br><span class="line"><span class="built_in">sudo</span> iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE</span><br><span class="line"><span class="built_in">sudo</span> iptables -A FORWARD -i wg0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT</span><br><span class="line"><span class="built_in">sudo</span> iptables -A FORWARD -i eth0 -o wg0 -j ACCEPT</span><br></pre></td></tr></table></figure><p>Сделайте правила iptables постоянными после перезагрузки:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install iptables-persistent -y</span><br></pre></td></tr></table></figure><p>Во время установки выберите “Да” для сохранения текущих правил IPv4 и IPv6.</p><p>Чтобы сохранить правила позже:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> netfilter-persistent save</span><br></pre></td></tr></table></figure><h2 id="Настройка-сети"><a href="#Настройка-сети" class="headerlink" title="Настройка сети"></a>Настройка сети</h2><h3 id="Настройка-статического-IP-адреса"><a href="#Настройка-статического-IP-адреса" class="headerlink" title="Настройка статического IP адреса"></a>Настройка статического IP адреса</h3><p>Для надежной маршрутизации VPN установите статический IP адрес для вашего Raspberry Pi:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> nano /etc/dhcpcd.conf</span><br></pre></td></tr></table></figure><p>Добавьте в конец файла (настройте значения для вашей сети):</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"># Конфигурация статического IP</span><br><span class="line">interface eth0</span><br><span class="line">static ip_address=192.168.1.100/24</span><br><span class="line">static routers=192.168.1.1</span><br><span class="line">static domain_name_servers=192.168.1.1 8.8.8.8</span><br><span class="line"></span><br><span class="line"># Для WiFi используйте wlan0 вместо eth0</span><br><span class="line"># interface wlan0</span><br><span class="line"># static ip_address=192.168.1.101/24</span><br><span class="line"># static routers=192.168.1.1</span><br><span class="line"># static domain_name_servers=192.168.1.1 8.8.8.8</span><br></pre></td></tr></table></figure><p><strong>Замените на значения вашей сети:</strong></p><ul><li><code>192.168.1.100</code> - Желаемый статический IP для вашего Pi</li><li><code>192.168.1.1</code> - IP вашего роутера (шлюз)</li><li><code>8.8.8.8</code> - Вторичный DNS сервер (Google DNS)</li></ul><p>Перезапустите сеть:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart dhcpcd</span><br></pre></td></tr></table></figure><h2 id="Устранение-неполадок"><a href="#Устранение-неполадок" class="headerlink" title="Устранение неполадок"></a>Устранение неполадок</h2><h3 id="Проверка-статуса-VPN"><a href="#Проверка-статуса-VPN" class="headerlink" title="Проверка статуса VPN"></a>Проверка статуса VPN</h3><p><strong>Для OpenVPN:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl status openvpn-client@client.service</span><br><span class="line">journalctl -u openvpn-client@client.service -f</span><br></pre></td></tr></table></figure><p><strong>Для WireGuard:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> wg show</span><br><span class="line">journalctl -u wg-quick@wg0 -f</span><br></pre></td></tr></table></figure><h3 id="Тестирование-подключения"><a href="#Тестирование-подключения" class="headerlink" title="Тестирование подключения"></a>Тестирование подключения</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Проверьте, существует ли интерфейс VPN</span></span><br><span class="line">ip addr show</span><br><span class="line"></span><br><span class="line"><span class="comment"># Проверьте таблицу маршрутизации</span></span><br><span class="line">ip route</span><br><span class="line"></span><br><span class="line"><span class="comment"># Проверьте интернет через VPN</span></span><br><span class="line">curl ifconfig.me</span><br><span class="line"></span><br><span class="line"><span class="comment"># Пингуйте VPN сервер</span></span><br><span class="line">ping 10.0.0.1  <span class="comment"># Замените на IP вашего VPN сервера</span></span><br></pre></td></tr></table></figure><h3 id="Частые-проблемы"><a href="#Частые-проблемы" class="headerlink" title="Частые проблемы"></a>Частые проблемы</h3><p><strong>OpenVPN не запускается:</strong></p><ul><li>Проверьте синтаксис файла конфигурации: <code>sudo openvpn --config /etc/openvpn/client/client.ovpn</code></li><li>Убедитесь, что сертификаты и ключи корректны</li><li>Проверьте настройки файрвола</li></ul><p><strong>Соединение WireGuard обрывается:</strong></p><ul><li>Добавьте <code>PersistentKeepalive = 25</code> в секцию [Peer]</li><li>Проверьте, доступна ли конечная точка сервера</li><li>Убедитесь, что файрвол разрешает UDP трафик на порту WireGuard</li></ul><p><strong>Нет интернета после подключения:</strong></p><ul><li>Проверьте настройки DNS в конфигурации VPN</li><li>Убедитесь, что <code>AllowedIPs = 0.0.0.0/0</code> для полного туннеля</li><li>Тест с помощью: <code>nslookup google.com</code></li></ul>]]></content>
    
    
    <summary type="html">Руководство по настройке VPN клиентов на Raspberry Pi для безопасного удаленного доступа</summary>
    
    
    
    <category term="Linux" scheme="https://devapro.github.io/categories/Linux/"/>
    
    
    <category term="linux" scheme="https://devapro.github.io/tags/linux/"/>
    
    <category term="raspberry pi" scheme="https://devapro.github.io/tags/raspberry-pi/"/>
    
    <category term="vpn" scheme="https://devapro.github.io/tags/vpn/"/>
    
    <category term="openvpn" scheme="https://devapro.github.io/tags/openvpn/"/>
    
    <category term="wireguard" scheme="https://devapro.github.io/tags/wireguard/"/>
    
  </entry>
  
  <entry>
    <title>How to Measure Android App Start-up Time</title>
    <link href="https://devapro.github.io/en/2024/11/19/android-app-startup-time-measurement/"/>
    <id>https://devapro.github.io/en/2024/11/19/android-app-startup-time-measurement/</id>
    <published>2024-11-19T17:30:00.000Z</published>
    <updated>2026-06-28T15:21:53.868Z</updated>
    
    <content type="html"><![CDATA[<p>App startup time is one of the most critical metrics for user experience. Users expect apps to launch quickly, and slow startup times directly correlate with user frustration, negative reviews, and app uninstalls. This guide covers how to accurately measure and track Android app startup performance.</p><h2 id="Why-Startup-Time-Matters"><a href="#Why-Startup-Time-Matters" class="headerlink" title="Why Startup Time Matters"></a>Why Startup Time Matters</h2><p><strong>User Experience Impact:</strong></p><ul><li>First impression of app quality</li><li>Directly affects user retention</li><li>Critical for app store ratings</li><li>Competitive advantage in crowded markets</li></ul><p><strong>Google’s Recommendations:</strong></p><ul><li><strong>Cold start</strong>: Should complete in &lt; 5 seconds</li><li><strong>Warm start</strong>: Should complete in &lt; 2 seconds</li><li><strong>Hot start</strong>: Should complete in &lt; 1.5 seconds</li></ul><p><strong>Business Impact:</strong></p><ul><li>1-second delay &#x3D; 7% reduction in conversions</li><li>53% of users abandon apps that take &gt; 3 seconds to load</li><li>App store rankings consider startup performance</li></ul><h2 id="Understanding-Startup-Types"><a href="#Understanding-Startup-Types" class="headerlink" title="Understanding Startup Types"></a>Understanding Startup Types</h2><h3 id="Cold-Start"><a href="#Cold-Start" class="headerlink" title="Cold Start"></a>Cold Start</h3><p><strong>What it is:</strong> App launched from scratch with no cached data</p><p><strong>When it happens:</strong></p><ul><li>First launch after device boot</li><li>App killed by system</li><li>User force-stopped the app</li></ul><p><strong>What’s measured:</strong></p><ul><li>Process creation</li><li>Application.onCreate()</li><li>First Activity creation and layout</li><li>First frame drawn</li></ul><p><strong>This is the most important metric</strong> - represents worst-case scenario.</p><h3 id="Warm-Start"><a href="#Warm-Start" class="headerlink" title="Warm Start"></a>Warm Start</h3><p><strong>What it is:</strong> App process exists but Activity was destroyed</p><p><strong>When it happens:</strong></p><ul><li>User pressed back button (Activity destroyed but process alive)</li><li>System reclaimed Activity due to memory pressure</li></ul><p><strong>What’s measured:</strong></p><ul><li>Activity recreation</li><li>Layout inflation</li><li>First frame drawn</li></ul><h3 id="Hot-Start"><a href="#Hot-Start" class="headerlink" title="Hot Start"></a>Hot Start</h3><p><strong>What it is:</strong> App already in memory, just brought to foreground</p><p><strong>When it happens:</strong></p><ul><li>User returns from recent apps</li><li>User pressed home and returns quickly</li></ul><p><strong>What’s measured:</strong></p><ul><li>Activity.onStart()</li><li>Activity.onResume()</li><li>Minimal work</li></ul><h2 id="Measurement-Guidelines"><a href="#Measurement-Guidelines" class="headerlink" title="Measurement Guidelines"></a>Measurement Guidelines</h2><h3 id="What-to-Measure"><a href="#What-to-Measure" class="headerlink" title="What to Measure"></a>What to Measure</h3><p><strong>Focus on Time to Initial Display (TTID):</strong></p><ul><li>From app launch to first visible frame</li><li>Excludes asynchronous data loading</li><li>Represents perceived startup time</li></ul><p><strong>Not included:</strong></p><ul><li>Network requests (unless blocking UI)</li><li>Background work that doesn’t block rendering</li><li>Splash screen animations (measure separately)</li></ul><h3 id="Best-Practices"><a href="#Best-Practices" class="headerlink" title="Best Practices"></a>Best Practices</h3><p><strong>1. Minimize External Factors</strong></p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ BAD: Network calls in Application.onCreate()</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyApp</span> : <span class="type">Application</span>() &#123;</span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">()</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate()</span><br><span class="line">        fetchRemoteConfig() <span class="comment">// Blocks startup!</span></span><br><span class="line">        initializeAnalytics().await() <span class="comment">// Blocks startup!</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ GOOD: Defer non-critical work</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyApp</span> : <span class="type">Application</span>() &#123;</span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">()</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate()</span><br><span class="line">        <span class="comment">// Critical initialization only</span></span><br><span class="line">        initializeCrashReporting() <span class="comment">// Fast, synchronous</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// Defer everything else</span></span><br><span class="line">        lifecycleScope.launch &#123;</span><br><span class="line">            fetchRemoteConfig()</span><br><span class="line">            initializeAnalytics()</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>2. Account for Device Variables</strong></p><p>Factors affecting measurements:</p><ul><li>CPU frequency and cores</li><li>Available RAM</li><li>System load (background apps)</li><li>Android version</li><li>Device thermal state</li></ul><p><strong>Solution:</strong> Test on multiple devices representing your user base.</p><p><strong>3. Track Relative Changes</strong></p><p>Instead of absolute times:</p><ul><li>Measure before&#x2F;after optimization</li><li>Track percentage improvement</li><li>Compare across app versions</li><li>Monitor trends over time</li></ul><p><strong>Example:</strong></p><ul><li>Before: 1.2s average startup</li><li>After optimization: 0.9s average</li><li><strong>Result: 25% improvement</strong> ← This is meaningful!</li></ul><h2 id="Method-1-ActivityManager-via-Logcat"><a href="#Method-1-ActivityManager-via-Logcat" class="headerlink" title="Method 1: ActivityManager via Logcat"></a>Method 1: ActivityManager via Logcat</h2><p>The simplest and most common method using Android’s built-in logging.</p><h3 id="Basic-Measurement"><a href="#Basic-Measurement" class="headerlink" title="Basic Measurement"></a>Basic Measurement</h3><p><strong>Step 1: Clear Logcat</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb logcat -c</span><br></pre></td></tr></table></figure><p><strong>Step 2: Launch Your App</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start-activity -W -n com.example.myapp/.MainActivity</span><br></pre></td></tr></table></figure><p><strong>Parameters explained:</strong></p><ul><li><code>-W</code>: Wait for launch to complete</li><li><code>-n</code>: Component name (package&#x2F;activity)</li></ul><p><strong>Step 3: View Results</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb logcat | grep <span class="string">&quot;Displayed&quot;</span></span><br></pre></td></tr></table></figure><p><strong>Sample output:</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ActivityManager: Displayed com.example.myapp/.MainActivity: +856ms</span><br></pre></td></tr></table></figure><p>This shows your app took <strong>856ms</strong> from launch to first frame.</p><h3 id="Cold-Start-Measurement"><a href="#Cold-Start-Measurement" class="headerlink" title="Cold Start Measurement"></a>Cold Start Measurement</h3><p>To ensure a true cold start:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. Force stop app</span></span><br><span class="line">adb shell am force-stop com.example.myapp</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. Clear app data (optional but recommended)</span></span><br><span class="line">adb shell pm clear com.example.myapp</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. Wait a moment for system to settle</span></span><br><span class="line"><span class="built_in">sleep</span> 2</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. Clear logcat</span></span><br><span class="line">adb logcat -c</span><br><span class="line"></span><br><span class="line"><span class="comment"># 5. Launch app</span></span><br><span class="line">adb shell am start-activity -W -n com.example.myapp/.MainActivity</span><br><span class="line"></span><br><span class="line"><span class="comment"># 6. Get results</span></span><br><span class="line">adb logcat | grep <span class="string">&quot;Displayed&quot;</span></span><br></pre></td></tr></table></figure><h3 id="Filtering-for-Specific-Activity"><a href="#Filtering-for-Specific-Activity" class="headerlink" title="Filtering for Specific Activity"></a>Filtering for Specific Activity</h3><p>If your app has multiple activities:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Only show results for SplashActivity</span></span><br><span class="line">adb logcat | grep <span class="string">&quot;Displayed.*SplashActivity&quot;</span></span><br></pre></td></tr></table></figure><p>This ensures you’re measuring the first activity only, not subsequent navigations.</p><h3 id="Full-Bash-Script"><a href="#Full-Bash-Script" class="headerlink" title="Full Bash Script"></a>Full Bash Script</h3><p>Save as <code>measure_startup.sh</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line">PACKAGE=<span class="string">&quot;com.example.myapp&quot;</span></span><br><span class="line">ACTIVITY=<span class="string">&quot;.MainActivity&quot;</span></span><br><span class="line">RUNS=10</span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Measuring cold start time for <span class="variable">$PACKAGE</span>&quot;</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Running <span class="variable">$RUNS</span> iterations...&quot;</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">total=0</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> $(<span class="built_in">seq</span> 1 <span class="variable">$RUNS</span>); <span class="keyword">do</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;Run <span class="variable">$i</span>/<span class="variable">$RUNS</span>...&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># Force stop app</span></span><br><span class="line">    adb shell am force-stop <span class="variable">$PACKAGE</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># Clear app data</span></span><br><span class="line">    adb shell pm clear <span class="variable">$PACKAGE</span> &gt; /dev/null 2&gt;&amp;1</span><br><span class="line"></span><br><span class="line">    <span class="comment"># Wait for system to settle</span></span><br><span class="line">    <span class="built_in">sleep</span> 2</span><br><span class="line"></span><br><span class="line">    <span class="comment"># Clear logcat</span></span><br><span class="line">    adb logcat -c</span><br><span class="line"></span><br><span class="line">    <span class="comment"># Launch app and capture time</span></span><br><span class="line">    adb shell am start-activity -W -n $PACKAGE<span class="variable">$ACTIVITY</span> &gt; /dev/null 2&gt;&amp;1</span><br><span class="line"></span><br><span class="line">    <span class="comment"># Extract startup time</span></span><br><span class="line">    <span class="keyword">time</span>=$(adb logcat -d | grep <span class="string">&quot;Displayed <span class="variable">$PACKAGE</span>&quot;</span> | <span class="built_in">tail</span> -1 | grep -oE <span class="string">&#x27;\+[0-9]+ms&#x27;</span> | grep -oE <span class="string">&#x27;[0-9]+&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> [ -n <span class="string">&quot;<span class="variable">$time</span>&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;  Time: <span class="variable">$&#123;time&#125;</span>ms&quot;</span></span><br><span class="line">        total=$((total + time))</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;  Failed to get time&quot;</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;&quot;</span></span><br><span class="line"><span class="keyword">done</span></span><br><span class="line"></span><br><span class="line">average=$((total / RUNS))</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Average cold start time: <span class="variable">$&#123;average&#125;</span>ms&quot;</span></span><br></pre></td></tr></table></figure><p><strong>Usage:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">chmod</span> +x measure_startup.sh</span><br><span class="line">./measure_startup.sh</span><br></pre></td></tr></table></figure><p><strong>Sample output:</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">Measuring cold start time for com.example.myapp</span><br><span class="line">Running 10 iterations...</span><br><span class="line"></span><br><span class="line">Run 1/10...</span><br><span class="line">  Time: 892ms</span><br><span class="line"></span><br><span class="line">Run 2/10...</span><br><span class="line">  Time: 856ms</span><br><span class="line"></span><br><span class="line">Run 3/10...</span><br><span class="line">  Time: 901ms</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line">Average cold start time: 873ms</span><br></pre></td></tr></table></figure><h2 id="Method-2-Android-Benchmark-Plugin"><a href="#Method-2-Android-Benchmark-Plugin" class="headerlink" title="Method 2: Android Benchmark Plugin"></a>Method 2: Android Benchmark Plugin</h2><p>More sophisticated approach using androidx.benchmark for precise measurements.</p><h3 id="Setup"><a href="#Setup" class="headerlink" title="Setup"></a>Setup</h3><p><strong>Step 1: Add Dependencies</strong></p><p>In <code>app/build.gradle</code>:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">dependencies</span> &#123;</span><br><span class="line">    <span class="comment">// Benchmark library</span></span><br><span class="line">    androidTestImplementation <span class="string">&quot;androidx.benchmark:benchmark-junit4:1.2.0&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">android &#123;</span><br><span class="line">    defaultConfig &#123;</span><br><span class="line">        testInstrumentationRunner <span class="string">&quot;androidx.benchmark.junit4.AndroidBenchmarkRunner&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Step 2: Configure Benchmark</strong></p><p>Create <code>app/src/androidTest/AndroidManifest.xml</code>:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;utf-8&quot;</span>?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">manifest</span> <span class="attr">xmlns:android</span>=<span class="string">&quot;http://schemas.android.com/apk/res/android&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">application</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- Declare benchmark activity --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">activity</span> <span class="attr">android:name</span>=<span class="string">&quot;androidx.benchmark.macro.MacrobenchmarkActivity&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">application</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">manifest</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>Step 3: Create Benchmark Test</strong></p><p><code>app/src/androidTest/java/com/example/StartupBenchmark.kt</code>:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RunWith(AndroidJUnit4::class)</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">StartupBenchmark</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@get:Rule</span></span><br><span class="line">    <span class="keyword">val</span> benchmarkRule = MacrobenchmarkRule()</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">startup</span><span class="params">()</span></span> = benchmarkRule.measureRepeated(</span><br><span class="line">        packageName = <span class="string">&quot;com.example.myapp&quot;</span>,</span><br><span class="line">        metrics = listOf(StartupTimingMetric()),</span><br><span class="line">        iterations = <span class="number">10</span>,</span><br><span class="line">        startupMode = StartupMode.COLD,</span><br><span class="line">        setupBlock = &#123;</span><br><span class="line">            <span class="comment">// Clear app data before each iteration</span></span><br><span class="line">            pressHome()</span><br><span class="line">        &#125;</span><br><span class="line">    ) &#123;</span><br><span class="line">        <span class="comment">// Launch app</span></span><br><span class="line">        startActivityAndWait()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Running-Benchmark"><a href="#Running-Benchmark" class="headerlink" title="Running Benchmark"></a>Running Benchmark</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Run benchmark test</span></span><br><span class="line">./gradlew :app:connectedAndroidTest</span><br><span class="line"></span><br><span class="line"><span class="comment"># Results are saved to:</span></span><br><span class="line"><span class="comment"># app/build/outputs/androidTest-results/</span></span><br></pre></td></tr></table></figure><p><strong>Sample output:</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">StartupBenchmark_startup</span><br><span class="line">  timeToInitialDisplayMs   min 789.2,   median 856.1,   max 943.7</span><br></pre></td></tr></table></figure><p><strong>Benefits over logcat method:</strong></p><ul><li>Statistical analysis (min, median, max, percentiles)</li><li>Automatic iteration handling</li><li>Consistent environment control</li><li>Integration with CI&#x2F;CD pipelines</li></ul><h3 id="Command-Line-Benchmark"><a href="#Command-Line-Benchmark" class="headerlink" title="Command-Line Benchmark"></a>Command-Line Benchmark</h3><p>For quick measurements without writing tests:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start-activity \</span><br><span class="line">  -W \</span><br><span class="line">  -a android.intent.action.VIEW \</span><br><span class="line">  -n com.example.myapp/.MainActivity \</span><br><span class="line">  --es <span class="string">&quot;androidx.benchmark.startupMode&quot;</span> <span class="string">&quot;COLD&quot;</span></span><br></pre></td></tr></table></figure><h2 id="Advanced-Startup-Profiling"><a href="#Advanced-Startup-Profiling" class="headerlink" title="Advanced: Startup Profiling"></a>Advanced: Startup Profiling</h2><p>For detailed analysis of what’s taking time:</p><h3 id="Using-Android-Studio-Profiler"><a href="#Using-Android-Studio-Profiler" class="headerlink" title="Using Android Studio Profiler"></a>Using Android Studio Profiler</h3><ol><li><strong>Run app in debug mode</strong></li><li><strong>Open Profiler</strong> (View → Tool Windows → Profiler)</li><li><strong>Click “+” and select your process</strong></li><li><strong>Click “CPU” and start recording</strong></li><li><strong>Force stop app</strong>: <code>adb shell am force-stop com.example.myapp</code></li><li><strong>Launch app</strong>: App should auto-attach to profiler</li><li><strong>Stop recording after first screen appears</strong></li></ol><p><strong>Analyze results:</strong></p><ul><li>Identify slow methods in Application.onCreate()</li><li>Find blocking I&#x2F;O operations</li><li>Detect unnecessary initialization</li></ul><h3 id="Using-Systrace"><a href="#Using-Systrace" class="headerlink" title="Using Systrace"></a>Using Systrace</h3><p>For system-level analysis:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Capture startup trace</span></span><br><span class="line">python systrace.py -t 10 -o startup_trace.html \</span><br><span class="line">  <span class="built_in">sched</span> freq idle am wm gfx view binder_driver hal dalvik \</span><br><span class="line">  camera input res &amp;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Launch app immediately after starting trace</span></span><br><span class="line"><span class="built_in">sleep</span> 1 &amp;&amp; adb shell am start-activity -W -n com.example.myapp/.MainActivity</span><br></pre></td></tr></table></figure><p>Open <code>startup_trace.html</code> in Chrome to analyze frame-by-frame rendering.</p><h2 id="Precision-Enhancement-Strategies"><a href="#Precision-Enhancement-Strategies" class="headerlink" title="Precision Enhancement Strategies"></a>Precision Enhancement Strategies</h2><h3 id="1-Clear-App-Data-Before-Each-Test"><a href="#1-Clear-App-Data-Before-Each-Test" class="headerlink" title="1. Clear App Data Before Each Test"></a>1. Clear App Data Before Each Test</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell pm clear com.example.myapp</span><br></pre></td></tr></table></figure><p><strong>Why:</strong> Ensures consistent state, removes cached data.</p><h3 id="2-Run-Multiple-Iterations"><a href="#2-Run-Multiple-Iterations" class="headerlink" title="2. Run Multiple Iterations"></a>2. Run Multiple Iterations</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Run 10 times and average</span></span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> &#123;1..10&#125;; <span class="keyword">do</span></span><br><span class="line">  <span class="comment"># measurement code</span></span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure><p><strong>Why:</strong> Accounts for variability, provides statistical confidence.</p><h3 id="3-Test-on-Same-Device"><a href="#3-Test-on-Same-Device" class="headerlink" title="3. Test on Same Device"></a>3. Test on Same Device</h3><p><strong>Why:</strong> Different devices have vastly different performance characteristics.</p><p><strong>Best practice:</strong></p><ul><li>Test on low-end device (represents worst case)</li><li>Test on mid-range device (represents majority)</li><li>Test on flagship (represents best case)</li></ul><h3 id="4-Lock-CPU-Frequency-Rooted-Devices"><a href="#4-Lock-CPU-Frequency-Rooted-Devices" class="headerlink" title="4. Lock CPU Frequency (Rooted Devices)"></a>4. Lock CPU Frequency (Rooted Devices)</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Requires root</span></span><br><span class="line">adb shell su -c <span class="string">&quot;echo performance &gt; /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor&quot;</span></span><br></pre></td></tr></table></figure><p><strong>Why:</strong> Prevents thermal throttling and frequency scaling from affecting results.</p><h3 id="5-Use-Real-Devices-Not-Emulators"><a href="#5-Use-Real-Devices-Not-Emulators" class="headerlink" title="5. Use Real Devices, Not Emulators"></a>5. Use Real Devices, Not Emulators</h3><p><strong>Why:</strong></p><ul><li>Emulators don’t accurately represent real device performance</li><li>Missing hardware acceleration</li><li>Different memory characteristics</li></ul><p><strong>Exception:</strong> Automated CI&#x2F;CD testing (accept that times won’t match real devices).</p><h3 id="6-Test-at-Same-Time-of-Day"><a href="#6-Test-at-Same-Time-of-Day" class="headerlink" title="6. Test at Same Time of Day"></a>6. Test at Same Time of Day</h3><p><strong>Why:</strong> Device background services vary by time (updates, syncs, etc.).</p><h3 id="7-Minimize-Background-Apps"><a href="#7-Minimize-Background-Apps" class="headerlink" title="7. Minimize Background Apps"></a>7. Minimize Background Apps</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Close all apps</span></span><br><span class="line">adb shell input keyevent KEYCODE_HOME</span><br><span class="line">adb shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS</span><br><span class="line"></span><br><span class="line"><span class="comment"># Wait for system to settle</span></span><br><span class="line"><span class="built_in">sleep</span> 5</span><br></pre></td></tr></table></figure><h2 id="Common-Startup-Performance-Issues"><a href="#Common-Startup-Performance-Issues" class="headerlink" title="Common Startup Performance Issues"></a>Common Startup Performance Issues</h2><h3 id="1-Heavy-Application-onCreate"><a href="#1-Heavy-Application-onCreate" class="headerlink" title="1. Heavy Application.onCreate()"></a>1. Heavy Application.onCreate()</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ BAD: Blocking operations</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyApp</span> : <span class="type">Application</span>() &#123;</span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">()</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate()</span><br><span class="line">        initDatabase() <span class="comment">// 200ms</span></span><br><span class="line">        setupAnalytics() <span class="comment">// 150ms</span></span><br><span class="line">        loadConfig() <span class="comment">// 100ms</span></span><br><span class="line">        <span class="comment">// Total: 450ms added to startup!</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ GOOD: Lazy initialization</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyApp</span> : <span class="type">Application</span>() &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> database <span class="keyword">by</span> lazy &#123; initDatabase() &#125;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> analytics <span class="keyword">by</span> lazy &#123; setupAnalytics() &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">()</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate()</span><br><span class="line">        <span class="comment">// Only critical crash reporting</span></span><br><span class="line">        Firebase.initialize(<span class="keyword">this</span>) <span class="comment">// 50ms</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-Synchronous-I-O"><a href="#2-Synchronous-I-O" class="headerlink" title="2. Synchronous I&#x2F;O"></a>2. Synchronous I&#x2F;O</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ BAD: Reading from disk on main thread</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MainActivity</span> : <span class="type">AppCompatActivity</span>() &#123;</span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line">        <span class="keyword">val</span> preferences = readPreferencesFromFile() <span class="comment">// Blocks!</span></span><br><span class="line">        setContentView(R.layout.activity_main)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ GOOD: Async loading with placeholder</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MainActivity</span> : <span class="type">AppCompatActivity</span>() &#123;</span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line">        setContentView(R.layout.activity_main) <span class="comment">// Show UI immediately</span></span><br><span class="line"></span><br><span class="line">        lifecycleScope.launch &#123;</span><br><span class="line">            <span class="keyword">val</span> preferences = withContext(Dispatchers.IO) &#123;</span><br><span class="line">                readPreferencesFromFile()</span><br><span class="line">            &#125;</span><br><span class="line">            updateUI(preferences)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-Complex-Layout-Inflation"><a href="#3-Complex-Layout-Inflation" class="headerlink" title="3. Complex Layout Inflation"></a>3. Complex Layout Inflation</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- ❌ BAD: Deeply nested layouts --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">LinearLayout</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">RelativeLayout</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">LinearLayout</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">RelativeLayout</span>&gt;</span></span><br><span class="line">                <span class="comment">&lt;!-- ... --&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">RelativeLayout</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">LinearLayout</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">RelativeLayout</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">LinearLayout</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- ✅ GOOD: Flat ConstraintLayout --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">androidx.constraintlayout.widget.ConstraintLayout</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- All views at same level --&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">androidx.constraintlayout.widget.ConstraintLayout</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="4-Custom-Font-Loading"><a href="#4-Custom-Font-Loading" class="headerlink" title="4. Custom Font Loading"></a>4. Custom Font Loading</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ BAD: Loading fonts synchronously</span></span><br><span class="line"><span class="keyword">val</span> typeface = ResourcesCompat.getFont(context, R.font.custom_font)</span><br><span class="line">textView.typeface = typeface</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ GOOD: Use XML font families (cached by system)</span></span><br><span class="line">&lt;!-- res/font/custom_font_family.xml --&gt;</span><br><span class="line">&lt;font-family&gt;</span><br><span class="line">    &lt;font android:font=<span class="string">&quot;@font/custom_font&quot;</span> /&gt;</span><br><span class="line">&lt;/font-family&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- Then <span class="keyword">in</span> layout: --&gt;</span><br><span class="line">android:fontFamily=<span class="string">&quot;@font/custom_font_family&quot;</span></span><br></pre></td></tr></table></figure><h2 id="Tracking-Over-Time"><a href="#Tracking-Over-Time" class="headerlink" title="Tracking Over Time"></a>Tracking Over Time</h2><h3 id="CI-CD-Integration"><a href="#CI-CD-Integration" class="headerlink" title="CI&#x2F;CD Integration"></a>CI&#x2F;CD Integration</h3><p>Add to your CI pipeline:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># .github/workflows/measure-startup.yml</span></span><br><span class="line"><span class="attr">name:</span> <span class="string">Measure</span> <span class="string">Startup</span> <span class="string">Time</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span> [<span class="string">pull_request</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">benchmark:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">macos-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v2</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span> <span class="string">startup</span> <span class="string">benchmark</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">./gradlew</span> <span class="string">:app:connectedAndroidTest</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">results</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v2</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">name:</span> <span class="string">benchmark-results</span></span><br><span class="line">          <span class="attr">path:</span> <span class="string">app/build/outputs/androidTest-results/</span></span><br></pre></td></tr></table></figure><h3 id="Firebase-Performance-Monitoring"><a href="#Firebase-Performance-Monitoring" class="headerlink" title="Firebase Performance Monitoring"></a>Firebase Performance Monitoring</h3><p>Track startup in production:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyApp</span> : <span class="type">Application</span>() &#123;</span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">()</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">val</span> trace = Firebase.performance.newTrace(<span class="string">&quot;app_start&quot;</span>)</span><br><span class="line">        trace.start()</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Your initialization code</span></span><br><span class="line"></span><br><span class="line">        trace.stop()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Benefits:</strong></p><ul><li>Real user measurements</li><li>Device distribution analysis</li><li>Geographic breakdown</li><li>Trend monitoring</li></ul><h2 id="Quick-Reference"><a href="#Quick-Reference" class="headerlink" title="Quick Reference"></a>Quick Reference</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Cold start measurement</span></span><br><span class="line">adb shell am force-stop com.example.myapp</span><br><span class="line">adb shell pm clear com.example.myapp</span><br><span class="line"><span class="built_in">sleep</span> 2</span><br><span class="line">adb logcat -c</span><br><span class="line">adb shell am start-activity -W -n com.example.myapp/.MainActivity</span><br><span class="line">adb logcat | grep <span class="string">&quot;Displayed&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Multiple iterations</span></span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> &#123;1..10&#125;; <span class="keyword">do</span></span><br><span class="line">  adb shell am force-stop com.example.myapp</span><br><span class="line">  adb shell pm clear com.example.myapp</span><br><span class="line">  <span class="built_in">sleep</span> 2</span><br><span class="line">  adb logcat -c</span><br><span class="line">  adb shell am start-activity -W -n com.example.myapp/.MainActivity</span><br><span class="line">  adb logcat | grep <span class="string">&quot;Displayed&quot;</span></span><br><span class="line"><span class="keyword">done</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Benchmark test</span></span><br><span class="line">./gradlew :app:connectedAndroidTest</span><br></pre></td></tr></table></figure><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>Measuring Android app startup time is essential for delivering great user experiences. Key takeaways:</p><ol><li><strong>Focus on cold start</strong> - It’s the worst-case scenario users experience</li><li><strong>Measure Time to Initial Display</strong> - From launch to first visible frame</li><li><strong>Use logcat for quick checks</strong> - Built-in and always available</li><li><strong>Use Benchmark library for precision</strong> - Statistical analysis and automation</li><li><strong>Run multiple iterations</strong> - Account for variability</li><li><strong>Clear app data between tests</strong> - Ensure consistent measurements</li><li><strong>Test on real devices</strong> - Emulators don’t represent real performance</li><li><strong>Track relative improvements</strong> - Percentage changes are more meaningful than absolute times</li><li><strong>Integrate with CI&#x2F;CD</strong> - Catch regressions before release</li><li><strong>Monitor in production</strong> - Real user metrics reveal the truth</li></ol><p>Remember: Every millisecond counts. Users notice the difference between 500ms and 1000ms startup times, even if they can’t quantify it. Faster startups lead to happier users, better ratings, and increased retention.</p><h2 id="Further-Reading"><a href="#Further-Reading" class="headerlink" title="Further Reading"></a>Further Reading</h2><ul><li><a href="https://developer.android.com/topic/performance/vitals/launch-time">Android Developer Guide - App Startup Time</a></li><li><a href="https://developer.android.com/studio/profile/benchmark">androidx.benchmark Documentation</a></li><li><a href="https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview">Macrobenchmark Guide</a></li><li><a href="https://developer.android.com/topic/libraries/app-startup">App Startup Library</a></li></ul>]]></content>
    
    
    <summary type="html">Complete guide to measuring Android app startup performance using logcat and benchmark tools for optimization</summary>
    
    
    
    <category term="Android" scheme="https://devapro.github.io/categories/Android/"/>
    
    <category term="Performance" scheme="https://devapro.github.io/categories/Android/Performance/"/>
    
    
    <category term="android" scheme="https://devapro.github.io/tags/android/"/>
    
    <category term="performance" scheme="https://devapro.github.io/tags/performance/"/>
    
    <category term="optimization" scheme="https://devapro.github.io/tags/optimization/"/>
    
    <category term="adb" scheme="https://devapro.github.io/tags/adb/"/>
    
  </entry>
  
  <entry>
    <title>Understanding Koin Scopes for Android Dependency Injection</title>
    <link href="https://devapro.github.io/en/2023/12/21/koin-scopes/"/>
    <id>https://devapro.github.io/en/2023/12/21/koin-scopes/</id>
    <published>2023-12-21T17:00:00.000Z</published>
    <updated>2026-06-28T15:21:53.869Z</updated>
    
    <content type="html"><![CDATA[<p>Dependency injection is crucial for modern Android development, and Koin offers a lightweight solution. One of its most powerful but often misunderstood features is <strong>scopes</strong> - a way to manage dependencies with lifetimes shorter than your app’s lifetime.</p><h2 id="The-Problem-Sharing-Objects-with-Custom-Lifetimes"><a href="#The-Problem-Sharing-Objects-with-Custom-Lifetimes" class="headerlink" title="The Problem: Sharing Objects with Custom Lifetimes"></a>The Problem: Sharing Objects with Custom Lifetimes</h2><p>When building Android apps, we often need to share objects between components (Activities, Fragments, ViewModels) that have lifetimes between a singleton and factory:</p><ul><li><strong>Too broad</strong>: Singletons live for the entire app lifetime, causing memory leaks if they hold references to Activities or Fragments</li><li><strong>Too narrow</strong>: Factories create new instances every time, preventing object sharing between components</li><li><strong>Just right</strong>: Scopes provide lifecycle-bound singletons that can be destroyed when no longer needed</li></ul><p>Common scenarios:</p><ul><li>Sharing data between Fragments in the same Activity</li><li>Passing state between screens in a flow (e.g., multi-step form, checkout process)</li><li>Managing feature-specific dependencies that shouldn’t be singletons</li></ul><h2 id="Koin-Scope-Types"><a href="#Koin-Scope-Types" class="headerlink" title="Koin Scope Types"></a>Koin Scope Types</h2><p>Koin provides three ways to create instances:</p><h3 id="1-Single-App-Lifetime-Singleton"><a href="#1-Single-App-Lifetime-Singleton" class="headerlink" title="1. Single - App Lifetime Singleton"></a>1. Single - App Lifetime Singleton</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">module &#123;</span><br><span class="line">    single &#123; DatabaseHelper() &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>Created once when first requested</li><li>Lives for the entire app lifetime</li><li>Never destroyed until app is killed</li><li><strong>Use for</strong>: Database, API clients, app-wide managers</li></ul><h3 id="2-Factory-New-Instance-Every-Time"><a href="#2-Factory-New-Instance-Every-Time" class="headerlink" title="2. Factory - New Instance Every Time"></a>2. Factory - New Instance Every Time</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">module &#123;</span><br><span class="line">    factory &#123; UserRepository() &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>Creates a new instance on every injection</li><li>No caching or sharing</li><li><strong>Use for</strong>: Stateless objects, lightweight classes</li></ul><h3 id="3-Scoped-Destroyable-Singleton"><a href="#3-Scoped-Destroyable-Singleton" class="headerlink" title="3. Scoped - Destroyable Singleton"></a>3. Scoped - Destroyable Singleton</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">module &#123;</span><br><span class="line">    scope(named(<span class="string">&quot;checkoutScope&quot;</span>)) &#123;</span><br><span class="line">        scoped &#123; CheckoutState() &#125;</span><br><span class="line">        scoped &#123; PaymentProcessor() &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>Acts like a singleton within the scope lifetime</li><li>Multiple components can share the same instance</li><li>Manually created and destroyed</li><li><strong>Use for</strong>: Feature-specific dependencies, flow state management</li></ul><p><strong>Key insight</strong>: Scoped instances function as singletons with the ability to be destroyed.</p><h2 id="Basic-Scope-Usage"><a href="#Basic-Scope-Usage" class="headerlink" title="Basic Scope Usage"></a>Basic Scope Usage</h2><h3 id="Step-1-Define-a-Scope-in-Koin-Module"><a href="#Step-1-Define-a-Scope-in-Koin-Module" class="headerlink" title="Step 1: Define a Scope in Koin Module"></a>Step 1: Define a Scope in Koin Module</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> checkoutModule = module &#123;</span><br><span class="line">    scope(named(<span class="string">&quot;checkoutScope&quot;</span>)) &#123;</span><br><span class="line">        scoped &#123; CheckoutState() &#125;</span><br><span class="line">        scoped &#123; ShippingCalculator() &#125;</span><br><span class="line">        scoped &#123; PaymentProcessor(<span class="keyword">get</span>()) &#125; <span class="comment">// Can inject other dependencies</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Important details:</strong></p><ul><li><code>named(&quot;checkoutScope&quot;)</code> creates a scope qualifier</li><li>Multiple scoped dependencies can be defined in the same scope</li><li>Dependencies can be injected using <code>get()</code> as usual</li></ul><h3 id="Step-2-Create-the-Scope"><a href="#Step-2-Create-the-Scope" class="headerlink" title="Step 2: Create the Scope"></a>Step 2: Create the Scope</h3><p>In your Activity or Fragment:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CheckoutActivity</span> : <span class="type">AppCompatActivity</span>() &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">lateinit</span> <span class="keyword">var</span> scope: Scope</span><br><span class="line"></span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Create and store scope reference</span></span><br><span class="line">        scope = getKoin().createScope(<span class="string">&quot;uniqueScopeId&quot;</span>, named(<span class="string">&quot;checkoutScope&quot;</span>))</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Get scoped instance</span></span><br><span class="line">        <span class="keyword">val</span> checkoutState: CheckoutState = scope.<span class="keyword">get</span>()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Key points:</strong></p><ul><li>First parameter: unique scope ID (can be any string)</li><li>Second parameter: scope qualifier from module definition</li><li>Store scope reference to close it later</li></ul><h3 id="Step-3-Share-Scope-Between-Components"><a href="#Step-3-Share-Scope-Between-Components" class="headerlink" title="Step 3: Share Scope Between Components"></a>Step 3: Share Scope Between Components</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ShippingFragment</span> : <span class="type">Fragment</span>() &#123;</span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onViewCreated</span><span class="params">(view: <span class="type">View</span>, savedInstanceState: <span class="type">Bundle</span>?)</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onViewCreated(view, savedInstanceState)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Retrieve existing scope by ID</span></span><br><span class="line">        <span class="keyword">val</span> scope = getKoin().getScope(<span class="string">&quot;uniqueScopeId&quot;</span>)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Get the same CheckoutState instance as CheckoutActivity</span></span><br><span class="line">        <span class="keyword">val</span> checkoutState: CheckoutState = scope.<span class="keyword">get</span>()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Both components now share the same <code>CheckoutState</code> instance!</p><h3 id="Step-4-Close-the-Scope"><a href="#Step-4-Close-the-Scope" class="headerlink" title="Step 4: Close the Scope"></a>Step 4: Close the Scope</h3><p><strong>Critical</strong>: Always close scopes to prevent memory leaks:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CheckoutActivity</span> : <span class="type">AppCompatActivity</span>() &#123;</span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onDestroy</span><span class="params">()</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onDestroy()</span><br><span class="line">        scope.close() <span class="comment">// Destroys all scoped instances</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Without closing</strong>: Scoped instances behave like singletons and never get garbage collected.</p><h2 id="Advanced-Lifecycle-Aware-Extension-Functions"><a href="#Advanced-Lifecycle-Aware-Extension-Functions" class="headerlink" title="Advanced: Lifecycle-Aware Extension Functions"></a>Advanced: Lifecycle-Aware Extension Functions</h2><p>Manual scope management is error-prone. Create extension functions that automatically handle scope lifecycle:</p><h3 id="Fragment-Scope-Extensions"><a href="#Fragment-Scope-Extensions" class="headerlink" title="Fragment Scope Extensions"></a>Fragment Scope Extensions</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Creates or retrieves a scope tied to this Fragment&#x27;s lifecycle</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> Fragment.<span class="title">getOrCreateScope</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">    scopeId: <span class="type">String</span>? = <span class="literal">null</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">    scopeName: <span class="type">Qualifier</span></span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span>: Scope &#123;</span><br><span class="line">    <span class="keyword">val</span> id = scopeId ?: <span class="keyword">this</span>::<span class="keyword">class</span>.java.name</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">try</span> &#123;</span><br><span class="line">        getKoin().getScope(id)</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e: Exception) &#123;</span><br><span class="line">        <span class="keyword">val</span> newScope = getKoin().createScope(id, scopeName)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Automatically close scope when Fragment is destroyed</span></span><br><span class="line">        lifecycle.addObserver(<span class="keyword">object</span> : DefaultLifecycleObserver &#123;</span><br><span class="line">            <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onDestroy</span><span class="params">(owner: <span class="type">LifecycleOwner</span>)</span></span> &#123;</span><br><span class="line">                newScope.close()</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;)</span><br><span class="line"></span><br><span class="line">        newScope</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Links this Fragment&#x27;s scope to a parent scope for dependency resolution</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> Fragment.<span class="title">linkScopeToActivity</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">val</span> activityScope = (requireActivity() <span class="keyword">as</span>? MainActivity)?.scope</span><br><span class="line">    activityScope?.let &#123; parentScope -&gt;</span><br><span class="line">        getOrCreateScope(scopeName = named(<span class="string">&quot;fragmentScope&quot;</span>)).apply &#123;</span><br><span class="line">            linkTo(parentScope)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Activity-Scope-Extensions"><a href="#Activity-Scope-Extensions" class="headerlink" title="Activity Scope Extensions"></a>Activity Scope Extensions</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Creates or retrieves a scope tied to this Activity&#x27;s lifecycle</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> AppCompatActivity.<span class="title">getOrCreateScope</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">    scopeId: <span class="type">String</span>? = <span class="literal">null</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">    scopeName: <span class="type">Qualifier</span></span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span>: Scope &#123;</span><br><span class="line">    <span class="keyword">val</span> id = scopeId ?: <span class="keyword">this</span>::<span class="keyword">class</span>.java.name</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">try</span> &#123;</span><br><span class="line">        getKoin().getScope(id)</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e: Exception) &#123;</span><br><span class="line">        <span class="keyword">val</span> newScope = getKoin().createScope(id, scopeName)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Automatically close scope when Activity is destroyed</span></span><br><span class="line">        lifecycle.addObserver(<span class="keyword">object</span> : DefaultLifecycleObserver &#123;</span><br><span class="line">            <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onDestroy</span><span class="params">(owner: <span class="type">LifecycleOwner</span>)</span></span> &#123;</span><br><span class="line">                newScope.close()</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;)</span><br><span class="line"></span><br><span class="line">        newScope</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Usage-with-Extensions"><a href="#Usage-with-Extensions" class="headerlink" title="Usage with Extensions"></a>Usage with Extensions</h3><p>Now scope management becomes much cleaner:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CheckoutActivity</span> : <span class="type">AppCompatActivity</span>() &#123;</span><br><span class="line">    <span class="keyword">val</span> scope: Scope <span class="keyword">by</span> lazy &#123;</span><br><span class="line">        getOrCreateScope(scopeName = named(<span class="string">&quot;checkoutScope&quot;</span>))</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Scope is automatically created and will be closed on destroy</span></span><br><span class="line">        <span class="keyword">val</span> checkoutState: CheckoutState = scope.<span class="keyword">get</span>()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ShippingFragment</span> : <span class="type">Fragment</span>() &#123;</span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onViewCreated</span><span class="params">(view: <span class="type">View</span>, savedInstanceState: <span class="type">Bundle</span>?)</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onViewCreated(view, savedInstanceState)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Link to Activity&#x27;s scope to access Activity-scoped dependencies</span></span><br><span class="line">        linkScopeToActivity()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">val</span> fragmentScope = getOrCreateScope(scopeName = named(<span class="string">&quot;fragmentScope&quot;</span>))</span><br><span class="line">        <span class="keyword">val</span> sharedState: CheckoutState = fragmentScope.<span class="keyword">get</span>()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Scope-Linking"><a href="#Scope-Linking" class="headerlink" title="Scope Linking"></a>Scope Linking</h2><p>Link scopes to create dependency hierarchies:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> appModule = module &#123;</span><br><span class="line">    scope(named(<span class="string">&quot;activityScope&quot;</span>)) &#123;</span><br><span class="line">        scoped &#123; ActivityDependency() &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    scope(named(<span class="string">&quot;fragmentScope&quot;</span>)) &#123;</span><br><span class="line">        scoped &#123; FragmentDependency(<span class="keyword">get</span>()) &#125; <span class="comment">// Can access parent scope</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// In code:</span></span><br><span class="line"><span class="keyword">val</span> activityScope = getKoin().createScope(<span class="string">&quot;activity1&quot;</span>, named(<span class="string">&quot;activityScope&quot;</span>))</span><br><span class="line"><span class="keyword">val</span> fragmentScope = getKoin().createScope(<span class="string">&quot;fragment1&quot;</span>, named(<span class="string">&quot;fragmentScope&quot;</span>))</span><br><span class="line"></span><br><span class="line"><span class="comment">// Link fragment scope to activity scope</span></span><br><span class="line">fragmentScope.linkTo(activityScope)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Now fragmentScope can access dependencies from activityScope</span></span><br></pre></td></tr></table></figure><p><strong>Benefits:</strong></p><ul><li>Fragment can access Activity-scoped dependencies</li><li>Maintains proper lifecycle boundaries</li><li>Enables parent-child dependency relationships</li></ul><h2 id="Practical-Example-Multi-Step-Checkout-Flow"><a href="#Practical-Example-Multi-Step-Checkout-Flow" class="headerlink" title="Practical Example: Multi-Step Checkout Flow"></a>Practical Example: Multi-Step Checkout Flow</h2><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Define modules</span></span><br><span class="line"><span class="keyword">val</span> checkoutModule = module &#123;</span><br><span class="line">    scope(named(<span class="string">&quot;checkoutScope&quot;</span>)) &#123;</span><br><span class="line">        scoped &#123; CheckoutState() &#125;</span><br><span class="line">        scoped &#123; CartManager() &#125;</span><br><span class="line">        scoped &#123; PaymentProcessor() &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Scope definition</span></span><br><span class="line"><span class="keyword">data</span> <span class="keyword">class</span> <span class="title class_">CheckoutState</span>(</span><br><span class="line">    <span class="keyword">var</span> shippingAddress: Address? = <span class="literal">null</span>,</span><br><span class="line">    <span class="keyword">var</span> paymentMethod: PaymentMethod? = <span class="literal">null</span>,</span><br><span class="line">    <span class="keyword">var</span> items: List&lt;CartItem&gt; = emptyList()</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Activity manages scope</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">CheckoutActivity</span> : <span class="type">AppCompatActivity</span>() &#123;</span><br><span class="line">    <span class="keyword">val</span> scope: Scope <span class="keyword">by</span> lazy &#123;</span><br><span class="line">        getOrCreateScope(scopeName = named(<span class="string">&quot;checkoutScope&quot;</span>))</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> checkoutState: CheckoutState <span class="keyword">by</span> lazy &#123; scope.<span class="keyword">get</span>() &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line">        setContentView(R.layout.activity_checkout)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Navigate through checkout steps</span></span><br><span class="line">        showFragment(ShippingFragment())</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Scope automatically closed on destroy via lifecycle observer</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Fragment 1: Shipping</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ShippingFragment</span> : <span class="type">Fragment</span>() &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> checkoutState: CheckoutState <span class="keyword">by</span> lazy &#123;</span><br><span class="line">        (requireActivity() <span class="keyword">as</span> CheckoutActivity).scope.<span class="keyword">get</span>()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">onShippingConfirmed</span><span class="params">(address: <span class="type">Address</span>)</span></span> &#123;</span><br><span class="line">        checkoutState.shippingAddress = address</span><br><span class="line">        <span class="comment">// Navigate to payment</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Fragment 2: Payment</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PaymentFragment</span> : <span class="type">Fragment</span>() &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> checkoutState: CheckoutState <span class="keyword">by</span> lazy &#123;</span><br><span class="line">        (requireActivity() <span class="keyword">as</span> CheckoutActivity).scope.<span class="keyword">get</span>()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> paymentProcessor: PaymentProcessor <span class="keyword">by</span> lazy &#123;</span><br><span class="line">        (requireActivity() <span class="keyword">as</span> CheckoutActivity).scope.<span class="keyword">get</span>()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">fun</span> <span class="title">onPaymentConfirmed</span><span class="params">(method: <span class="type">PaymentMethod</span>)</span></span> &#123;</span><br><span class="line">        checkoutState.paymentMethod = method</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Process payment with access to all checkout data</span></span><br><span class="line">        paymentProcessor.process(checkoutState)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>Benefits of this approach:</strong></p><ul><li>All fragments share the same <code>CheckoutState</code> instance</li><li>State persists across fragment transactions</li><li>Everything is cleaned up when Activity is destroyed</li><li>No manual scope management needed with extension functions</li></ul><h2 id="Common-Pitfalls"><a href="#Common-Pitfalls" class="headerlink" title="Common Pitfalls"></a>Common Pitfalls</h2><h3 id="1-Forgetting-to-Close-Scopes"><a href="#1-Forgetting-to-Close-Scopes" class="headerlink" title="1. Forgetting to Close Scopes"></a>1. Forgetting to Close Scopes</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ BAD: Memory leak</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyActivity</span> : <span class="type">AppCompatActivity</span>() &#123;</span><br><span class="line">    <span class="keyword">val</span> scope = getKoin().createScope(<span class="string">&quot;myScope&quot;</span>, named(<span class="string">&quot;activityScope&quot;</span>))</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Forgot to close scope!</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ GOOD: Use lifecycle observer</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyActivity</span> : <span class="type">AppCompatActivity</span>() &#123;</span><br><span class="line">    <span class="keyword">val</span> scope: Scope <span class="keyword">by</span> lazy &#123;</span><br><span class="line">        getOrCreateScope(scopeName = named(<span class="string">&quot;activityScope&quot;</span>))</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// Automatically closed via extension function</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-Creating-Multiple-Scopes-with-Same-ID"><a href="#2-Creating-Multiple-Scopes-with-Same-ID" class="headerlink" title="2. Creating Multiple Scopes with Same ID"></a>2. Creating Multiple Scopes with Same ID</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ BAD: Will throw exception</span></span><br><span class="line"><span class="keyword">val</span> scope1 = getKoin().createScope(<span class="string">&quot;checkout&quot;</span>, named(<span class="string">&quot;checkoutScope&quot;</span>))</span><br><span class="line"><span class="keyword">val</span> scope2 = getKoin().createScope(<span class="string">&quot;checkout&quot;</span>, named(<span class="string">&quot;checkoutScope&quot;</span>)) <span class="comment">// Crash!</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ GOOD: Use unique IDs or check existence</span></span><br><span class="line"><span class="keyword">val</span> scope = <span class="keyword">try</span> &#123;</span><br><span class="line">    getKoin().getScope(<span class="string">&quot;checkout&quot;</span>)</span><br><span class="line">&#125; <span class="keyword">catch</span> (e: Exception) &#123;</span><br><span class="line">    getKoin().createScope(<span class="string">&quot;checkout&quot;</span>, named(<span class="string">&quot;checkoutScope&quot;</span>))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-Accessing-Scope-After-Closing"><a href="#3-Accessing-Scope-After-Closing" class="headerlink" title="3. Accessing Scope After Closing"></a>3. Accessing Scope After Closing</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ BAD: Will throw exception</span></span><br><span class="line">scope.close()</span><br><span class="line"><span class="keyword">val</span> instance = scope.<span class="keyword">get</span>&lt;MyDependency&gt;() <span class="comment">// Crash!</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ GOOD: Don&#x27;t access closed scopes</span></span><br><span class="line">scope.close()</span><br><span class="line"><span class="comment">// Don&#x27;t use scope after this point</span></span><br></pre></td></tr></table></figure><h2 id="When-to-Use-Scopes"><a href="#When-to-Use-Scopes" class="headerlink" title="When to Use Scopes"></a>When to Use Scopes</h2><p><strong>Use scopes when:</strong></p><ul><li>✅ You need to share state between multiple components</li><li>✅ Components have the same or nested lifetimes</li><li>✅ You want automatic cleanup when flow completes</li><li>✅ Dependencies shouldn’t be app-wide singletons</li></ul><p><strong>Don’t use scopes when:</strong></p><ul><li>❌ Dependency is truly app-wide (use <code>single</code> instead)</li><li>❌ No sharing needed (use <code>factory</code> instead)</li><li>❌ Managing lifecycle is too complex (consider other patterns)</li></ul><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>Koin scopes provide a powerful mechanism for managing dependencies with custom lifetimes. Key takeaways:</p><ol><li><strong>Scopes &#x3D; destroyable singletons</strong> - Share instances within a lifecycle boundary</li><li><strong>Manual management required</strong> - Must create and close scopes explicitly</li><li><strong>Lifecycle observers help</strong> - Use extension functions for automatic cleanup</li><li><strong>Scope linking enables hierarchies</strong> - Parent scopes can provide dependencies to children</li><li><strong>Always close scopes</strong> - Prevent memory leaks by closing scopes when done</li></ol><p>Scopes bridge the gap between singletons (too broad) and factories (too narrow), giving you precise control over dependency lifetimes in your Android applications.</p><h2 id="Further-Reading"><a href="#Further-Reading" class="headerlink" title="Further Reading"></a>Further Reading</h2><ul><li><a href="https://insert-koin.io/docs/reference/koin-core/scopes">Koin Official Documentation - Scopes</a></li><li><a href="https://github.com/InsertKoinIO/koin">Koin GitHub Repository</a></li><li><a href="https://developer.android.com/training/dependency-injection">Dependency Injection Best Practices</a></li></ul>]]></content>
    
    
    <summary type="html">Complete guide to using Koin scopes for managing dependencies with custom lifetimes in Android applications</summary>
    
    
    
    <category term="Android" scheme="https://devapro.github.io/categories/Android/"/>
    
    
    <category term="android" scheme="https://devapro.github.io/tags/android/"/>
    
    <category term="kotlin" scheme="https://devapro.github.io/tags/kotlin/"/>
    
    <category term="koin" scheme="https://devapro.github.io/tags/koin/"/>
    
    <category term="dependency-injection" scheme="https://devapro.github.io/tags/dependency-injection/"/>
    
    <category term="architecture" scheme="https://devapro.github.io/tags/architecture/"/>
    
  </entry>
  
  <entry>
    <title>Complete Disk Backup and Restore with dd Command</title>
    <link href="https://devapro.github.io/en/2020/06/24/Full-back-up-with-dd/"/>
    <id>https://devapro.github.io/en/2020/06/24/Full-back-up-with-dd/</id>
    <published>2020-06-24T13:27:40.000Z</published>
    <updated>2026-06-28T15:21:53.868Z</updated>
    
    <content type="html"><![CDATA[<p>The <code>dd</code> command is a powerful Linux utility for creating exact bit-by-bit copies of disks, partitions, or files. This guide covers how to safely create full disk backups and restore them when needed.</p><h2 id="⚠️-Important-Safety-Warning"><a href="#⚠️-Important-Safety-Warning" class="headerlink" title="⚠️ Important Safety Warning"></a>⚠️ Important Safety Warning</h2><p><strong>dd is dangerous!</strong> A single typo can permanently destroy your data. Always:</p><ul><li>Double-check your input (<code>if=</code>) and output (<code>of=</code>) devices</li><li>Ensure you’re backing up the correct disk</li><li>Never run dd on a mounted filesystem</li><li>Keep backups on a separate physical drive</li><li>Test your backups before you need them</li></ul><p>The <code>dd</code> command is nicknamed “disk destroyer” for good reason—use it carefully!</p><h2 id="Prerequisites"><a href="#Prerequisites" class="headerlink" title="Prerequisites"></a>Prerequisites</h2><ul><li>Root or sudo access</li><li>Sufficient disk space for backup (at least equal to source disk size)</li><li>External drive or network storage for backup storage</li><li>Basic understanding of Linux disk naming (&#x2F;dev&#x2F;sda, &#x2F;dev&#x2F;sdb, etc.)</li></ul><h2 id="Finding-Your-Disks"><a href="#Finding-Your-Disks" class="headerlink" title="Finding Your Disks"></a>Finding Your Disks</h2><h3 id="List-All-Disks"><a href="#List-All-Disks" class="headerlink" title="List All Disks"></a>List All Disks</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># List all disks and partitions</span></span><br><span class="line"><span class="built_in">sudo</span> fdisk -l</span><br><span class="line"></span><br><span class="line"><span class="comment"># Or use lsblk for a tree view</span></span><br><span class="line">lsblk</span><br><span class="line"></span><br><span class="line"><span class="comment"># Check disk usage</span></span><br><span class="line"><span class="built_in">df</span> -h</span><br></pre></td></tr></table></figure><p><strong>Understanding disk names:</strong></p><ul><li><code>/dev/sda</code> - First SATA&#x2F;SCSI disk (entire disk)</li><li><code>/dev/sda1</code> - First partition on first disk</li><li><code>/dev/nvme0n1</code> - First NVMe SSD</li><li><code>/dev/mmcblk0</code> - SD card</li><li><code>/dev/sdb</code> - Second disk (usually external USB)</li></ul><p><strong>Important:</strong> Back up the entire disk (e.g., <code>/dev/sda</code>), not just a partition (e.g., <code>/dev/sda1</code>). This ensures bootloader and partition table are included.</p><h3 id="Identify-Your-Source-Disk"><a href="#Identify-Your-Source-Disk" class="headerlink" title="Identify Your Source Disk"></a>Identify Your Source Disk</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Check mounted disks</span></span><br><span class="line">mount | grep <span class="string">&quot;^/dev&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Get detailed disk information</span></span><br><span class="line"><span class="built_in">sudo</span> fdisk -l /dev/sda</span><br></pre></td></tr></table></figure><h2 id="Creating-a-Disk-Backup"><a href="#Creating-a-Disk-Backup" class="headerlink" title="Creating a Disk Backup"></a>Creating a Disk Backup</h2><h3 id="Basic-Disk-to-Image-Backup"><a href="#Basic-Disk-to-Image-Backup" class="headerlink" title="Basic Disk to Image Backup"></a>Basic Disk to Image Backup</h3><p>Create a complete disk image file:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Backup entire disk to image file</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/path/to/backup/full_disk_backup.img bs=4M status=progress</span><br></pre></td></tr></table></figure><p><strong>Parameters explained:</strong></p><ul><li><code>if=/dev/sda</code> - Input file (source disk to backup)</li><li><code>of=full_disk_backup.img</code> - Output file (backup image)</li><li><code>bs=4M</code> - Block size of 4 megabytes (faster than default)</li><li><code>status=progress</code> - Shows progress during copy</li></ul><p><strong>Example with real paths:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Backup to external USB drive</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/media/usb/backups/laptop_backup_2025-12-29.img bs=4M status=progress</span><br></pre></td></tr></table></figure><h3 id="Direct-Disk-to-Disk-Backup"><a href="#Direct-Disk-to-Disk-Backup" class="headerlink" title="Direct Disk to Disk Backup"></a>Direct Disk to Disk Backup</h3><p>Clone one disk directly to another (faster, no intermediate file):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Copy disk /dev/sda to disk /dev/sdb</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/dev/sdb bs=4M status=progress</span><br></pre></td></tr></table></figure><p><strong>Warning:</strong> This will completely erase <code>/dev/sdb</code>! Triple-check your device names!</p><h3 id="Compressed-Backup-Saves-Space"><a href="#Compressed-Backup-Saves-Space" class="headerlink" title="Compressed Backup (Saves Space)"></a>Compressed Backup (Saves Space)</h3><p>Compress the backup on-the-fly using gzip or pigz:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Compress with gzip (slower, better compression)</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M status=progress | gzip -c &gt; /path/to/backup/disk_backup.img.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># Compress with pigz (parallel gzip, faster on multi-core CPUs)</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M status=progress | pigz -c &gt; /path/to/backup/disk_backup.img.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># Lower compression for speed (level 1-9, default 6)</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M status=progress | gzip -1 &gt; /path/to/backup/disk_backup.img.gz</span><br></pre></td></tr></table></figure><p><strong>Compression comparison:</strong></p><ul><li>No compression: Fastest, but huge file (entire disk size)</li><li>gzip -9: Slowest, smallest file (~30-50% reduction)</li><li>pigz -1: Good balance of speed and size</li></ul><h3 id="Backup-Only-Used-Space-with-dd-rescue"><a href="#Backup-Only-Used-Space-with-dd-rescue" class="headerlink" title="Backup Only Used Space (with dd_rescue)"></a>Backup Only Used Space (with dd_rescue)</h3><p>For large disks with little data, use <code>ddrescue</code> to skip empty blocks:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Install ddrescue</span></span><br><span class="line"><span class="built_in">sudo</span> apt install gddrescue</span><br><span class="line"></span><br><span class="line"><span class="comment"># Backup with intelligent copying</span></span><br><span class="line"><span class="built_in">sudo</span> ddrescue -f -n /dev/sda /path/to/backup/disk_backup.img /path/to/backup/disk_backup.log</span><br></pre></td></tr></table></figure><h2 id="Optimizing-dd-Performance"><a href="#Optimizing-dd-Performance" class="headerlink" title="Optimizing dd Performance"></a>Optimizing dd Performance</h2><h3 id="Choosing-Block-Size"><a href="#Choosing-Block-Size" class="headerlink" title="Choosing Block Size"></a>Choosing Block Size</h3><p>Block size affects speed significantly:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Too small (slow)</span></span><br><span class="line">bs=512    <span class="comment"># 512 bytes - very slow</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Good choices (fast)</span></span><br><span class="line">bs=4M     <span class="comment"># 4 megabytes - good default</span></span><br><span class="line">bs=8M     <span class="comment"># 8 megabytes - faster for large disks</span></span><br><span class="line">bs=16M    <span class="comment"># 16 megabytes - fastest, but uses more RAM</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Separate read/write sizes</span></span><br><span class="line">bs=4M conv=<span class="built_in">sync</span>,noerror  <span class="comment"># Continue on read errors</span></span><br></pre></td></tr></table></figure><p><strong>Recommendation:</strong> Use <code>bs=4M</code> for most cases, <code>bs=8M</code> or <code>bs=16M</code> for very large disks.</p><h3 id="Monitor-Progress"><a href="#Monitor-Progress" class="headerlink" title="Monitor Progress"></a>Monitor Progress</h3><p>If you forgot <code>status=progress</code>, monitor dd in another terminal:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Find dd process ID</span></span><br><span class="line">ps aux | grep <span class="built_in">dd</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Send USR1 signal to show progress</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">kill</span> -USR1 &lt;pid&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Or use this one-liner</span></span><br><span class="line">watch -n 5 <span class="string">&#x27;sudo kill -USR1 $(pgrep ^dd$)&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="Speed-Test"><a href="#Speed-Test" class="headerlink" title="Speed Test"></a>Speed Test</h3><p>Benchmark your disk before backing up:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Write speed test (creates 1GB test file)</span></span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/zero of=/path/to/testfile bs=1M count=1024 oflag=direct</span><br><span class="line"></span><br><span class="line"><span class="comment"># Read speed test</span></span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/path/to/testfile of=/dev/null bs=1M count=1024 iflag=direct</span><br><span class="line"></span><br><span class="line"><span class="comment"># Clean up</span></span><br><span class="line"><span class="built_in">rm</span> /path/to/testfile</span><br></pre></td></tr></table></figure><h2 id="Restoring-from-Backup"><a href="#Restoring-from-Backup" class="headerlink" title="Restoring from Backup"></a>Restoring from Backup</h2><h3 id="Restore-Image-to-Disk"><a href="#Restore-Image-to-Disk" class="headerlink" title="Restore Image to Disk"></a>Restore Image to Disk</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Restore uncompressed image</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/path/to/backup/full_disk_backup.img of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Restore compressed image (gzip)</span></span><br><span class="line">gunzip -c /path/to/backup/disk_backup.img.gz | <span class="built_in">sudo</span> <span class="built_in">dd</span> of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Restore compressed image (pigz)</span></span><br><span class="line">pigz -dc /path/to/backup/disk_backup.img.gz | <span class="built_in">sudo</span> <span class="built_in">dd</span> of=/dev/sdb bs=4M status=progress</span><br></pre></td></tr></table></figure><p><strong>Critical reminders:</strong></p><ul><li><code>/dev/sdb</code> will be completely overwritten</li><li>Unmount the target disk first</li><li>For boot disks, connect only ONE boot disk at a time</li><li>Verify device names with <code>lsblk</code> before running</li></ul><h3 id="Restore-Specific-Partition"><a href="#Restore-Specific-Partition" class="headerlink" title="Restore Specific Partition"></a>Restore Specific Partition</h3><p>To restore just one partition:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Backup single partition</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda1 of=/path/to/backup/partition_backup.img bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Restore single partition</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/path/to/backup/partition_backup.img of=/dev/sdb1 bs=4M status=progress</span><br></pre></td></tr></table></figure><h2 id="Verification"><a href="#Verification" class="headerlink" title="Verification"></a>Verification</h2><p>Always verify your backups!</p><h3 id="Compare-Backup-to-Original"><a href="#Compare-Backup-to-Original" class="headerlink" title="Compare Backup to Original"></a>Compare Backup to Original</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Calculate checksums</span></span><br><span class="line"><span class="built_in">md5sum</span> /dev/sda &gt; original.md5</span><br><span class="line"><span class="built_in">md5sum</span> /path/to/backup/disk_backup.img &gt; backup.md5</span><br><span class="line"></span><br><span class="line"><span class="comment"># Compare</span></span><br><span class="line">diff original.md5 backup.md5</span><br><span class="line"></span><br><span class="line"><span class="comment"># Or use cmp for bit-by-bit comparison</span></span><br><span class="line"><span class="built_in">sudo</span> cmp /dev/sda /path/to/backup/disk_backup.img</span><br></pre></td></tr></table></figure><h3 id="Mount-and-Test-Backup"><a href="#Mount-and-Test-Backup" class="headerlink" title="Mount and Test Backup"></a>Mount and Test Backup</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Create loop device from backup</span></span><br><span class="line"><span class="built_in">sudo</span> losetup -f -P /path/to/backup/disk_backup.img</span><br><span class="line"></span><br><span class="line"><span class="comment"># Check which loop device was created</span></span><br><span class="line"><span class="built_in">sudo</span> losetup -a</span><br><span class="line"></span><br><span class="line"><span class="comment"># Mount partition from backup (assume /dev/loop0p1)</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">mkdir</span> /mnt/backup_test</span><br><span class="line"><span class="built_in">sudo</span> mount /dev/loop0p1 /mnt/backup_test</span><br><span class="line"></span><br><span class="line"><span class="comment"># Browse backup</span></span><br><span class="line"><span class="built_in">ls</span> /mnt/backup_test</span><br><span class="line"></span><br><span class="line"><span class="comment"># Unmount and cleanup</span></span><br><span class="line"><span class="built_in">sudo</span> umount /mnt/backup_test</span><br><span class="line"><span class="built_in">sudo</span> losetup -d /dev/loop0</span><br></pre></td></tr></table></figure><h2 id="Safety-Best-Practices"><a href="#Safety-Best-Practices" class="headerlink" title="Safety Best Practices"></a>Safety Best Practices</h2><h3 id="Before-Creating-Backup"><a href="#Before-Creating-Backup" class="headerlink" title="Before Creating Backup"></a>Before Creating Backup</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. Unmount the source if possible</span></span><br><span class="line"><span class="built_in">sudo</span> umount /dev/sda1</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. Check available space on destination</span></span><br><span class="line"><span class="built_in">df</span> -h /path/to/backup</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. Test destination is writable</span></span><br><span class="line"><span class="built_in">touch</span> /path/to/backup/test &amp;&amp; <span class="built_in">rm</span> /path/to/backup/test</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. Double-check device names</span></span><br><span class="line">lsblk</span><br></pre></td></tr></table></figure><h3 id="Before-Restoring-Backup"><a href="#Before-Restoring-Backup" class="headerlink" title="Before Restoring Backup"></a>Before Restoring Backup</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. List all disks to confirm target</span></span><br><span class="line">lsblk</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. Unmount target disk</span></span><br><span class="line"><span class="built_in">sudo</span> umount /dev/sdb*</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. Confirm you have the right backup</span></span><br><span class="line"><span class="built_in">ls</span> -lh /path/to/backup/</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. TRIPLE CHECK device names!</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Restoring to: /dev/sdb&quot;</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Press Ctrl+C to cancel or Enter to continue&quot;</span></span><br><span class="line"><span class="built_in">read</span></span><br></pre></td></tr></table></figure><h2 id="Common-Use-Cases"><a href="#Common-Use-Cases" class="headerlink" title="Common Use Cases"></a>Common Use Cases</h2><h3 id="Backup-Before-System-Upgrade"><a href="#Backup-Before-System-Upgrade" class="headerlink" title="Backup Before System Upgrade"></a>Backup Before System Upgrade</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Backup boot disk before major upgrade</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/media/external/pre-upgrade-backup.img bs=4M status=progress</span><br></pre></td></tr></table></figure><h3 id="Clone-Disk-to-Larger-Disk"><a href="#Clone-Disk-to-Larger-Disk" class="headerlink" title="Clone Disk to Larger Disk"></a>Clone Disk to Larger Disk</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. Clone to larger disk</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. Expand partition to use new space</span></span><br><span class="line"><span class="built_in">sudo</span> parted /dev/sdb</span><br><span class="line">(parted) <span class="built_in">print</span> free</span><br><span class="line">(parted) resizepart 2 100%</span><br><span class="line">(parted) quit</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. Resize filesystem</span></span><br><span class="line"><span class="built_in">sudo</span> resize2fs /dev/sdb2  <span class="comment"># For ext4</span></span><br><span class="line"><span class="comment"># or</span></span><br><span class="line"><span class="built_in">sudo</span> xfs_growfs /dev/sdb2  <span class="comment"># For XFS</span></span><br></pre></td></tr></table></figure><h3 id="Backup-SD-Card-Raspberry-Pi"><a href="#Backup-SD-Card-Raspberry-Pi" class="headerlink" title="Backup SD Card (Raspberry Pi)"></a>Backup SD Card (Raspberry Pi)</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Find SD card device (usually /dev/mmcblk0 or /dev/sdb)</span></span><br><span class="line">lsblk</span><br><span class="line"></span><br><span class="line"><span class="comment"># Backup SD card</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mmcblk0 of=~/backups/raspberrypi_backup.img bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Compress</span></span><br><span class="line">gzip ~/backups/raspberrypi_backup.img</span><br><span class="line"></span><br><span class="line"><span class="comment"># Restore to new SD card</span></span><br><span class="line">gunzip -c ~/backups/raspberrypi_backup.img.gz | <span class="built_in">sudo</span> <span class="built_in">dd</span> of=/dev/mmcblk0 bs=4M status=progress</span><br></pre></td></tr></table></figure><h3 id="Create-Bootable-USB-from-ISO"><a href="#Create-Bootable-USB-from-ISO" class="headerlink" title="Create Bootable USB from ISO"></a>Create Bootable USB from ISO</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Copy ISO to USB drive</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=~/Downloads/ubuntu-24.04.iso of=/dev/sdb bs=4M status=progress oflag=<span class="built_in">sync</span></span><br></pre></td></tr></table></figure><h2 id="Troubleshooting"><a href="#Troubleshooting" class="headerlink" title="Troubleshooting"></a>Troubleshooting</h2><h3 id="“No-space-left-on-device”"><a href="#“No-space-left-on-device”" class="headerlink" title="“No space left on device”"></a>“No space left on device”</h3><p><strong>Problem:</strong> Destination doesn’t have enough space.</p><p><strong>Solution:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Check available space</span></span><br><span class="line"><span class="built_in">df</span> -h /path/to/backup</span><br><span class="line"></span><br><span class="line"><span class="comment"># Use compression to reduce size</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M | gzip &gt; backup.img.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># Or backup to network drive</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M | ssh user@server <span class="string">&#x27;cat &gt; /backups/disk.img&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="“Device-is-busy”"><a href="#“Device-is-busy”" class="headerlink" title="“Device is busy”"></a>“Device is busy”</h3><p><strong>Problem:</strong> Disk is mounted or in use.</p><p><strong>Solution:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Check what&#x27;s using the device</span></span><br><span class="line"><span class="built_in">sudo</span> lsof | grep /dev/sda</span><br><span class="line"></span><br><span class="line"><span class="comment"># Unmount all partitions</span></span><br><span class="line"><span class="built_in">sudo</span> umount /dev/sda*</span><br><span class="line"></span><br><span class="line"><span class="comment"># Force unmount if needed</span></span><br><span class="line"><span class="built_in">sudo</span> umount -l /dev/sda1</span><br></pre></td></tr></table></figure><h3 id="“Operation-not-permitted”"><a href="#“Operation-not-permitted”" class="headerlink" title="“Operation not permitted”"></a>“Operation not permitted”</h3><p><strong>Problem:</strong> Insufficient permissions.</p><p><strong>Solution:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Run with sudo</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> ...</span><br><span class="line"></span><br><span class="line"><span class="comment"># Check if disk is write-protected</span></span><br><span class="line">hdparm -r /dev/sda</span><br></pre></td></tr></table></figure><h3 id="dd-Seems-Stuck"><a href="#dd-Seems-Stuck" class="headerlink" title="dd Seems Stuck"></a>dd Seems Stuck</h3><p><strong>Problem:</strong> No progress visible.</p><p><strong>Solution:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Check if dd is actually running</span></span><br><span class="line">ps aux | grep <span class="built_in">dd</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Send signal to show progress</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">kill</span> -USR1 $(pgrep ^<span class="built_in">dd</span>$)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Monitor I/O</span></span><br><span class="line">iostat -x 2</span><br></pre></td></tr></table></figure><h3 id="Backup-File-Too-Large"><a href="#Backup-File-Too-Large" class="headerlink" title="Backup File Too Large"></a>Backup File Too Large</h3><p><strong>Problem:</strong> Image file is huge.</p><p><strong>Solution:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Use compression</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M | gzip -1 &gt; backup.img.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># Or backup only used blocks with partclone</span></span><br><span class="line"><span class="built_in">sudo</span> partclone.ext4 -c -s /dev/sda1 -o backup.partclone</span><br><span class="line"></span><br><span class="line"><span class="comment"># Or use rsync for incremental backups</span></span><br><span class="line"><span class="built_in">sudo</span> rsync -aAXv --delete / /media/backup/</span><br></pre></td></tr></table></figure><h2 id="Alternative-Tools"><a href="#Alternative-Tools" class="headerlink" title="Alternative Tools"></a>Alternative Tools</h2><p>While <code>dd</code> is powerful, consider these alternatives for specific needs:</p><p><strong>For system backups:</strong></p><ul><li><code>rsync</code> - Incremental backups, faster than dd for used space only</li><li><code>tar</code> - Archive specific directories</li><li><code>Clonezilla</code> - GUI tool, intelligent cloning</li><li><code>timeshift</code> - System snapshots (like macOS Time Machine)</li></ul><p><strong>For disk cloning:</strong></p><ul><li><code>ddrescue</code> - Recovers data from failing disks</li><li><code>partclone</code> - Copies only used blocks (smaller backups)</li><li><code>fsarchiver</code> - Filesystem-level backup</li></ul><p><strong>For disk imaging:</strong></p><ul><li><code>partimage</code> - Creates partition images</li><li><code>g4u</code> - Ghost for Unix</li><li><code>redo rescue</code> - Live CD for backups</li></ul><h2 id="Quick-Reference"><a href="#Quick-Reference" class="headerlink" title="Quick Reference"></a>Quick Reference</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Basic backup</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=backup.img bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Compressed backup</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M | gzip &gt; backup.img.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># Direct clone</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Restore from backup</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=backup.img of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Restore from compressed backup</span></span><br><span class="line">gunzip -c backup.img.gz | <span class="built_in">sudo</span> <span class="built_in">dd</span> of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Show progress of running dd</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">kill</span> -USR1 $(pgrep ^<span class="built_in">dd</span>$)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Verify backup</span></span><br><span class="line"><span class="built_in">md5sum</span> /dev/sda backup.img</span><br><span class="line"></span><br><span class="line"><span class="comment"># Mount backup image</span></span><br><span class="line"><span class="built_in">sudo</span> losetup -f -P backup.img</span><br><span class="line"><span class="built_in">sudo</span> mount /dev/loop0p1 /mnt/test</span><br></pre></td></tr></table></figure><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>The <code>dd</code> command is an essential tool for complete disk backups and disaster recovery. Key takeaways:</p><ul><li>Always verify device names before running dd</li><li>Use compression to save space (<code>gzip</code> or <code>pigz</code>)</li><li>Add <code>status=progress</code> to monitor progress</li><li>Test your backups before you need them</li><li>Keep backups on separate physical drives</li><li>Consider alternatives like <code>rsync</code> for incremental backups</li></ul><p>Remember: dd is powerful but dangerous. One wrong command can destroy your data permanently. Always double-check your commands before pressing Enter!</p>]]></content>
    
    
    <summary type="html">Complete guide to creating and restoring full disk backups using dd utility with compression and verification</summary>
    
    
    
    <category term="Linux" scheme="https://devapro.github.io/categories/Linux/"/>
    
    
    <category term="linux" scheme="https://devapro.github.io/tags/linux/"/>
    
    <category term="dd" scheme="https://devapro.github.io/tags/dd/"/>
    
    <category term="backup" scheme="https://devapro.github.io/tags/backup/"/>
    
    <category term="recovery" scheme="https://devapro.github.io/tags/recovery/"/>
    
  </entry>
  
  <entry>
    <title>Полная резервная копия и восстановление диска с помощью команды dd</title>
    <link href="https://devapro.github.io/ru/2020/06/24/polnaya-rezervnaya-kopiya-diska-dd/"/>
    <id>https://devapro.github.io/ru/2020/06/24/polnaya-rezervnaya-kopiya-diska-dd/</id>
    <published>2020-06-24T13:27:40.000Z</published>
    <updated>2026-06-28T15:21:53.870Z</updated>
    
    <content type="html"><![CDATA[<p>Команда <code>dd</code> - это мощная утилита Linux для создания точных побитовых копий дисков, разделов или файлов. Это руководство охватывает, как безопасно создавать полные резервные копии дисков и восстанавливать их при необходимости.</p><h2 id="⚠️-Важное-предупреждение-о-безопасности"><a href="#⚠️-Важное-предупреждение-о-безопасности" class="headerlink" title="⚠️ Важное предупреждение о безопасности"></a>⚠️ Важное предупреждение о безопасности</h2><p><strong>dd опасна!</strong> Одна опечатка может навсегда уничтожить ваши данные. Всегда:</p><ul><li>Перепроверяйте входное (<code>if=</code>) и выходное (<code>of=</code>) устройства</li><li>Убедитесь, что делаете резервную копию правильного диска</li><li>Никогда не запускайте dd на смонтированной файловой системе</li><li>Храните резервные копии на отдельном физическом диске</li><li>Тестируйте резервные копии до того, как они понадобятся</li></ul><p>Команду <code>dd</code> не зря называют «disk destroyer» (разрушитель дисков) — используйте её осторожно!</p><h2 id="Предварительные-требования"><a href="#Предварительные-требования" class="headerlink" title="Предварительные требования"></a>Предварительные требования</h2><ul><li>Root или sudo доступ</li><li>Достаточное дисковое пространство для резервной копии (как минимум равное размеру исходного диска)</li><li>Внешний диск или сетевое хранилище для размещения резервной копии</li><li>Базовое понимание именования дисков в Linux (&#x2F;dev&#x2F;sda, &#x2F;dev&#x2F;sdb и т.д.)</li></ul><h2 id="Поиск-дисков"><a href="#Поиск-дисков" class="headerlink" title="Поиск дисков"></a>Поиск дисков</h2><h3 id="Список-всех-дисков"><a href="#Список-всех-дисков" class="headerlink" title="Список всех дисков"></a>Список всех дисков</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Список всех дисков и разделов</span></span><br><span class="line"><span class="built_in">sudo</span> fdisk -l</span><br><span class="line"></span><br><span class="line"><span class="comment"># Или используйте lsblk для древовидного представления</span></span><br><span class="line">lsblk</span><br><span class="line"></span><br><span class="line"><span class="comment"># Проверка использования диска</span></span><br><span class="line"><span class="built_in">df</span> -h</span><br></pre></td></tr></table></figure><p><strong>Понимание имён дисков:</strong></p><ul><li><code>/dev/sda</code> - Первый SATA&#x2F;SCSI диск (весь диск)</li><li><code>/dev/sda1</code> - Первый раздел на первом диске</li><li><code>/dev/nvme0n1</code> - Первый NVMe SSD</li><li><code>/dev/mmcblk0</code> - SD карта</li><li><code>/dev/sdb</code> - Второй диск (обычно внешний USB)</li></ul><p><strong>Важно:</strong> Делайте резервную копию всего диска (например, <code>/dev/sda</code>), а не только раздела (например, <code>/dev/sda1</code>). Это гарантирует включение загрузчика и таблицы разделов.</p><h3 id="Определение-исходного-диска"><a href="#Определение-исходного-диска" class="headerlink" title="Определение исходного диска"></a>Определение исходного диска</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Проверка смонтированных дисков</span></span><br><span class="line">mount | grep <span class="string">&quot;^/dev&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Получение детальной информации о диске</span></span><br><span class="line"><span class="built_in">sudo</span> fdisk -l /dev/sda</span><br></pre></td></tr></table></figure><h2 id="Создание-резервной-копии-диска"><a href="#Создание-резервной-копии-диска" class="headerlink" title="Создание резервной копии диска"></a>Создание резервной копии диска</h2><h3 id="Базовая-резервная-копия-диска-в-образ"><a href="#Базовая-резервная-копия-диска-в-образ" class="headerlink" title="Базовая резервная копия диска в образ"></a>Базовая резервная копия диска в образ</h3><p>Создание полного образа диска:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Резервная копия всего диска в файл образа</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/path/to/backup/full_disk_backup.img bs=4M status=progress</span><br></pre></td></tr></table></figure><p><strong>Объяснение параметров:</strong></p><ul><li><code>if=/dev/sda</code> - Входной файл (исходный диск для резервной копии)</li><li><code>of=full_disk_backup.img</code> - Выходной файл (образ резервной копии)</li><li><code>bs=4M</code> - Размер блока 4 мегабайта (быстрее, чем по умолчанию)</li><li><code>status=progress</code> - Показывает прогресс во время копирования</li></ul><p><strong>Пример с реальными путями:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Резервная копия на внешний USB диск</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/media/usb/backups/laptop_backup_2025-12-29.img bs=4M status=progress</span><br></pre></td></tr></table></figure><h3 id="Прямая-резервная-копия-диск-в-диск"><a href="#Прямая-резервная-копия-диск-в-диск" class="headerlink" title="Прямая резервная копия диск-в-диск"></a>Прямая резервная копия диск-в-диск</h3><p>Клонирование одного диска напрямую на другой (быстрее, без промежуточного файла):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Копирование диска /dev/sda на диск /dev/sdb</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/dev/sdb bs=4M status=progress</span><br></pre></td></tr></table></figure><p><strong>Предупреждение:</strong> Это полностью сотрёт <code>/dev/sdb</code>! Трижды проверьте имена устройств!</p><h3 id="Сжатая-резервная-копия-экономит-место"><a href="#Сжатая-резервная-копия-экономит-место" class="headerlink" title="Сжатая резервная копия (экономит место)"></a>Сжатая резервная копия (экономит место)</h3><p>Сжатие резервной копии на лету с использованием gzip или pigz:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Сжатие с gzip (медленнее, лучше сжатие)</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M status=progress | gzip -c &gt; /path/to/backup/disk_backup.img.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># Сжатие с pigz (параллельный gzip, быстрее на многоядерных процессорах)</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M status=progress | pigz -c &gt; /path/to/backup/disk_backup.img.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># Меньшее сжатие для скорости (уровень 1-9, по умолчанию 6)</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M status=progress | gzip -1 &gt; /path/to/backup/disk_backup.img.gz</span><br></pre></td></tr></table></figure><p><strong>Сравнение сжатия:</strong></p><ul><li>Без сжатия: Быстрее всего, но огромный файл (размер всего диска)</li><li>gzip -9: Медленнее всего, самый маленький файл (~30-50% уменьшение)</li><li>pigz -1: Хороший баланс скорости и размера</li></ul><h3 id="Резервная-копия-только-используемого-пространства-с-dd-rescue"><a href="#Резервная-копия-только-используемого-пространства-с-dd-rescue" class="headerlink" title="Резервная копия только используемого пространства (с dd_rescue)"></a>Резервная копия только используемого пространства (с dd_rescue)</h3><p>Для больших дисков с малым количеством данных используйте <code>ddrescue</code> для пропуска пустых блоков:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Установка ddrescue</span></span><br><span class="line"><span class="built_in">sudo</span> apt install gddrescue</span><br><span class="line"></span><br><span class="line"><span class="comment"># Резервная копия с интеллектуальным копированием</span></span><br><span class="line"><span class="built_in">sudo</span> ddrescue -f -n /dev/sda /path/to/backup/disk_backup.img /path/to/backup/disk_backup.log</span><br></pre></td></tr></table></figure><h2 id="Оптимизация-производительности-dd"><a href="#Оптимизация-производительности-dd" class="headerlink" title="Оптимизация производительности dd"></a>Оптимизация производительности dd</h2><h3 id="Выбор-размера-блока"><a href="#Выбор-размера-блока" class="headerlink" title="Выбор размера блока"></a>Выбор размера блока</h3><p>Размер блока значительно влияет на скорость:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Слишком маленький (медленно)</span></span><br><span class="line">bs=512    <span class="comment"># 512 байт - очень медленно</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Хорошие варианты (быстро)</span></span><br><span class="line">bs=4M     <span class="comment"># 4 мегабайта - хорошее значение по умолчанию</span></span><br><span class="line">bs=8M     <span class="comment"># 8 мегабайт - быстрее для больших дисков</span></span><br><span class="line">bs=16M    <span class="comment"># 16 мегабайт - быстрее всего, но использует больше RAM</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Раздельные размеры чтения/записи</span></span><br><span class="line">bs=4M conv=<span class="built_in">sync</span>,noerror  <span class="comment"># Продолжить при ошибках чтения</span></span><br></pre></td></tr></table></figure><p><strong>Рекомендация:</strong> Используйте <code>bs=4M</code> в большинстве случаев, <code>bs=8M</code> или <code>bs=16M</code> для очень больших дисков.</p><h3 id="Мониторинг-прогресса"><a href="#Мониторинг-прогресса" class="headerlink" title="Мониторинг прогресса"></a>Мониторинг прогресса</h3><p>Если вы забыли <code>status=progress</code>, мониторьте dd в другом терминале:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Найти ID процесса dd</span></span><br><span class="line">ps aux | grep <span class="built_in">dd</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Отправить сигнал USR1 для показа прогресса</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">kill</span> -USR1 &lt;pid&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Или используйте этот однострочник</span></span><br><span class="line">watch -n 5 <span class="string">&#x27;sudo kill -USR1 $(pgrep ^dd$)&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="Тест-скорости"><a href="#Тест-скорости" class="headerlink" title="Тест скорости"></a>Тест скорости</h3><p>Бенчмарк вашего диска перед созданием резервной копии:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Тест скорости записи (создаёт тестовый файл 1GB)</span></span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/zero of=/path/to/testfile bs=1M count=1024 oflag=direct</span><br><span class="line"></span><br><span class="line"><span class="comment"># Тест скорости чтения</span></span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/path/to/testfile of=/dev/null bs=1M count=1024 iflag=direct</span><br><span class="line"></span><br><span class="line"><span class="comment"># Очистка</span></span><br><span class="line"><span class="built_in">rm</span> /path/to/testfile</span><br></pre></td></tr></table></figure><h2 id="Восстановление-из-резервной-копии"><a href="#Восстановление-из-резервной-копии" class="headerlink" title="Восстановление из резервной копии"></a>Восстановление из резервной копии</h2><h3 id="Восстановление-образа-на-диск"><a href="#Восстановление-образа-на-диск" class="headerlink" title="Восстановление образа на диск"></a>Восстановление образа на диск</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Восстановление несжатого образа</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/path/to/backup/full_disk_backup.img of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Восстановление сжатого образа (gzip)</span></span><br><span class="line">gunzip -c /path/to/backup/disk_backup.img.gz | <span class="built_in">sudo</span> <span class="built_in">dd</span> of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Восстановление сжатого образа (pigz)</span></span><br><span class="line">pigz -dc /path/to/backup/disk_backup.img.gz | <span class="built_in">sudo</span> <span class="built_in">dd</span> of=/dev/sdb bs=4M status=progress</span><br></pre></td></tr></table></figure><p><strong>Критически важные напоминания:</strong></p><ul><li><code>/dev/sdb</code> будет полностью перезаписан</li><li>Сначала размонтируйте целевой диск</li><li>Для загрузочных дисков подключайте только ОДИН загрузочный диск за раз</li><li>Проверьте имена устройств с помощью <code>lsblk</code> перед запуском</li></ul><h3 id="Восстановление-конкретного-раздела"><a href="#Восстановление-конкретного-раздела" class="headerlink" title="Восстановление конкретного раздела"></a>Восстановление конкретного раздела</h3><p>Для восстановления только одного раздела:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Резервная копия одного раздела</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda1 of=/path/to/backup/partition_backup.img bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Восстановление одного раздела</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/path/to/backup/partition_backup.img of=/dev/sdb1 bs=4M status=progress</span><br></pre></td></tr></table></figure><h2 id="Проверка"><a href="#Проверка" class="headerlink" title="Проверка"></a>Проверка</h2><p>Всегда проверяйте ваши резервные копии!</p><h3 id="Сравнение-резервной-копии-с-оригиналом"><a href="#Сравнение-резервной-копии-с-оригиналом" class="headerlink" title="Сравнение резервной копии с оригиналом"></a>Сравнение резервной копии с оригиналом</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Вычисление контрольных сумм</span></span><br><span class="line"><span class="built_in">md5sum</span> /dev/sda &gt; original.md5</span><br><span class="line"><span class="built_in">md5sum</span> /path/to/backup/disk_backup.img &gt; backup.md5</span><br><span class="line"></span><br><span class="line"><span class="comment"># Сравнение</span></span><br><span class="line">diff original.md5 backup.md5</span><br><span class="line"></span><br><span class="line"><span class="comment"># Или используйте cmp для побитового сравнения</span></span><br><span class="line"><span class="built_in">sudo</span> cmp /dev/sda /path/to/backup/disk_backup.img</span><br></pre></td></tr></table></figure><h3 id="Монтирование-и-тестирование-резервной-копии"><a href="#Монтирование-и-тестирование-резервной-копии" class="headerlink" title="Монтирование и тестирование резервной копии"></a>Монтирование и тестирование резервной копии</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Создание loop устройства из резервной копии</span></span><br><span class="line"><span class="built_in">sudo</span> losetup -f -P /path/to/backup/disk_backup.img</span><br><span class="line"></span><br><span class="line"><span class="comment"># Проверка какое loop устройство было создано</span></span><br><span class="line"><span class="built_in">sudo</span> losetup -a</span><br><span class="line"></span><br><span class="line"><span class="comment"># Монтирование раздела из резервной копии (предположим /dev/loop0p1)</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">mkdir</span> /mnt/backup_test</span><br><span class="line"><span class="built_in">sudo</span> mount /dev/loop0p1 /mnt/backup_test</span><br><span class="line"></span><br><span class="line"><span class="comment"># Просмотр резервной копии</span></span><br><span class="line"><span class="built_in">ls</span> /mnt/backup_test</span><br><span class="line"></span><br><span class="line"><span class="comment"># Размонтирование и очистка</span></span><br><span class="line"><span class="built_in">sudo</span> umount /mnt/backup_test</span><br><span class="line"><span class="built_in">sudo</span> losetup -d /dev/loop0</span><br></pre></td></tr></table></figure><h2 id="Лучшие-практики-безопасности"><a href="#Лучшие-практики-безопасности" class="headerlink" title="Лучшие практики безопасности"></a>Лучшие практики безопасности</h2><h3 id="Перед-созданием-резервной-копии"><a href="#Перед-созданием-резервной-копии" class="headerlink" title="Перед созданием резервной копии"></a>Перед созданием резервной копии</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. Размонтируйте источник, если возможно</span></span><br><span class="line"><span class="built_in">sudo</span> umount /dev/sda1</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. Проверьте доступное место на целевом диске</span></span><br><span class="line"><span class="built_in">df</span> -h /path/to/backup</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. Проверьте, что целевой диск доступен для записи</span></span><br><span class="line"><span class="built_in">touch</span> /path/to/backup/test &amp;&amp; <span class="built_in">rm</span> /path/to/backup/test</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. Перепроверьте имена устройств</span></span><br><span class="line">lsblk</span><br></pre></td></tr></table></figure><h3 id="Перед-восстановлением-резервной-копии"><a href="#Перед-восстановлением-резервной-копии" class="headerlink" title="Перед восстановлением резервной копии"></a>Перед восстановлением резервной копии</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. Список всех дисков для подтверждения целевого</span></span><br><span class="line">lsblk</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. Размонтируйте целевой диск</span></span><br><span class="line"><span class="built_in">sudo</span> umount /dev/sdb*</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. Подтвердите, что у вас правильная резервная копия</span></span><br><span class="line"><span class="built_in">ls</span> -lh /path/to/backup/</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. ТРИЖДЫ ПРОВЕРЬТЕ имена устройств!</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Восстановление на: /dev/sdb&quot;</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;Нажмите Ctrl+C для отмены или Enter для продолжения&quot;</span></span><br><span class="line"><span class="built_in">read</span></span><br></pre></td></tr></table></figure><h2 id="Типичные-случаи-использования"><a href="#Типичные-случаи-использования" class="headerlink" title="Типичные случаи использования"></a>Типичные случаи использования</h2><h3 id="Резервная-копия-перед-обновлением-системы"><a href="#Резервная-копия-перед-обновлением-системы" class="headerlink" title="Резервная копия перед обновлением системы"></a>Резервная копия перед обновлением системы</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Резервная копия загрузочного диска перед важным обновлением</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/media/external/pre-upgrade-backup.img bs=4M status=progress</span><br></pre></td></tr></table></figure><h3 id="Клонирование-диска-на-больший-диск"><a href="#Клонирование-диска-на-больший-диск" class="headerlink" title="Клонирование диска на больший диск"></a>Клонирование диска на больший диск</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. Клонирование на больший диск</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. Расширение раздела для использования нового пространства</span></span><br><span class="line"><span class="built_in">sudo</span> parted /dev/sdb</span><br><span class="line">(parted) <span class="built_in">print</span> free</span><br><span class="line">(parted) resizepart 2 100%</span><br><span class="line">(parted) quit</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. Изменение размера файловой системы</span></span><br><span class="line"><span class="built_in">sudo</span> resize2fs /dev/sdb2  <span class="comment"># Для ext4</span></span><br><span class="line"><span class="comment"># или</span></span><br><span class="line"><span class="built_in">sudo</span> xfs_growfs /dev/sdb2  <span class="comment"># Для XFS</span></span><br></pre></td></tr></table></figure><h3 id="Резервная-копия-SD-карты-Raspberry-Pi"><a href="#Резервная-копия-SD-карты-Raspberry-Pi" class="headerlink" title="Резервная копия SD карты (Raspberry Pi)"></a>Резервная копия SD карты (Raspberry Pi)</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Найти устройство SD карты (обычно /dev/mmcblk0 или /dev/sdb)</span></span><br><span class="line">lsblk</span><br><span class="line"></span><br><span class="line"><span class="comment"># Резервная копия SD карты</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mmcblk0 of=~/backups/raspberrypi_backup.img bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Сжатие</span></span><br><span class="line">gzip ~/backups/raspberrypi_backup.img</span><br><span class="line"></span><br><span class="line"><span class="comment"># Восстановление на новую SD карту</span></span><br><span class="line">gunzip -c ~/backups/raspberrypi_backup.img.gz | <span class="built_in">sudo</span> <span class="built_in">dd</span> of=/dev/mmcblk0 bs=4M status=progress</span><br></pre></td></tr></table></figure><h3 id="Создание-загрузочного-USB-из-ISO"><a href="#Создание-загрузочного-USB-из-ISO" class="headerlink" title="Создание загрузочного USB из ISO"></a>Создание загрузочного USB из ISO</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Копирование ISO на USB диск</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=~/Downloads/ubuntu-24.04.iso of=/dev/sdb bs=4M status=progress oflag=<span class="built_in">sync</span></span><br></pre></td></tr></table></figure><h2 id="Устранение-неполадок"><a href="#Устранение-неполадок" class="headerlink" title="Устранение неполадок"></a>Устранение неполадок</h2><h3 id="«No-space-left-on-device»"><a href="#«No-space-left-on-device»" class="headerlink" title="«No space left on device»"></a>«No space left on device»</h3><p><strong>Проблема:</strong> На целевом диске недостаточно места.</p><p><strong>Решение:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Проверка доступного места</span></span><br><span class="line"><span class="built_in">df</span> -h /path/to/backup</span><br><span class="line"></span><br><span class="line"><span class="comment"># Использование сжатия для уменьшения размера</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M | gzip &gt; backup.img.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># Или резервная копия на сетевой диск</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M | ssh user@server <span class="string">&#x27;cat &gt; /backups/disk.img&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="«Device-is-busy»"><a href="#«Device-is-busy»" class="headerlink" title="«Device is busy»"></a>«Device is busy»</h3><p><strong>Проблема:</strong> Диск смонтирован или используется.</p><p><strong>Решение:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Проверка, что использует устройство</span></span><br><span class="line"><span class="built_in">sudo</span> lsof | grep /dev/sda</span><br><span class="line"></span><br><span class="line"><span class="comment"># Размонтирование всех разделов</span></span><br><span class="line"><span class="built_in">sudo</span> umount /dev/sda*</span><br><span class="line"></span><br><span class="line"><span class="comment"># Принудительное размонтирование при необходимости</span></span><br><span class="line"><span class="built_in">sudo</span> umount -l /dev/sda1</span><br></pre></td></tr></table></figure><h3 id="«Operation-not-permitted»"><a href="#«Operation-not-permitted»" class="headerlink" title="«Operation not permitted»"></a>«Operation not permitted»</h3><p><strong>Проблема:</strong> Недостаточно прав доступа.</p><p><strong>Решение:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Запуск с sudo</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> ...</span><br><span class="line"></span><br><span class="line"><span class="comment"># Проверка, защищён ли диск от записи</span></span><br><span class="line">hdparm -r /dev/sda</span><br></pre></td></tr></table></figure><h3 id="dd-зависла"><a href="#dd-зависла" class="headerlink" title="dd зависла"></a>dd зависла</h3><p><strong>Проблема:</strong> Прогресс не виден.</p><p><strong>Решение:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Проверка, работает ли dd на самом деле</span></span><br><span class="line">ps aux | grep <span class="built_in">dd</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Отправка сигнала для показа прогресса</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">kill</span> -USR1 $(pgrep ^<span class="built_in">dd</span>$)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Мониторинг I/O</span></span><br><span class="line">iostat -x 2</span><br></pre></td></tr></table></figure><h3 id="Слишком-большой-файл-резервной-копии"><a href="#Слишком-большой-файл-резервной-копии" class="headerlink" title="Слишком большой файл резервной копии"></a>Слишком большой файл резервной копии</h3><p><strong>Проблема:</strong> Файл образа огромный.</p><p><strong>Решение:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Использование сжатия</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M | gzip -1 &gt; backup.img.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># Или резервная копия только используемых блоков с partclone</span></span><br><span class="line"><span class="built_in">sudo</span> partclone.ext4 -c -s /dev/sda1 -o backup.partclone</span><br><span class="line"></span><br><span class="line"><span class="comment"># Или использование rsync для инкрементных резервных копий</span></span><br><span class="line"><span class="built_in">sudo</span> rsync -aAXv --delete / /media/backup/</span><br></pre></td></tr></table></figure><h2 id="Альтернативные-инструменты"><a href="#Альтернативные-инструменты" class="headerlink" title="Альтернативные инструменты"></a>Альтернативные инструменты</h2><p>Хотя <code>dd</code> мощная, рассмотрите эти альтернативы для конкретных нужд:</p><p><strong>Для системных резервных копий:</strong></p><ul><li><code>rsync</code> - Инкрементные резервные копии, быстрее, чем dd только для используемого пространства</li><li><code>tar</code> - Архивирование конкретных директорий</li><li><code>Clonezilla</code> - GUI инструмент, интеллектуальное клонирование</li><li><code>timeshift</code> - Снимки системы (как macOS Time Machine)</li></ul><p><strong>Для клонирования дисков:</strong></p><ul><li><code>ddrescue</code> - Восстанавливает данные с отказывающих дисков</li><li><code>partclone</code> - Копирует только используемые блоки (меньшие резервные копии)</li><li><code>fsarchiver</code> - Резервная копия на уровне файловой системы</li></ul><p><strong>Для создания образов дисков:</strong></p><ul><li><code>partimage</code> - Создаёт образы разделов</li><li><code>g4u</code> - Ghost for Unix</li><li><code>redo rescue</code> - Live CD для резервных копий</li></ul><h2 id="Краткая-справка"><a href="#Краткая-справка" class="headerlink" title="Краткая справка"></a>Краткая справка</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Базовая резервная копия</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=backup.img bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Сжатая резервная копия</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda bs=4M | gzip &gt; backup.img.gz</span><br><span class="line"></span><br><span class="line"><span class="comment"># Прямое клонирование</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/sda of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Восстановление из резервной копии</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">dd</span> <span class="keyword">if</span>=backup.img of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Восстановление из сжатой резервной копии</span></span><br><span class="line">gunzip -c backup.img.gz | <span class="built_in">sudo</span> <span class="built_in">dd</span> of=/dev/sdb bs=4M status=progress</span><br><span class="line"></span><br><span class="line"><span class="comment"># Показать прогресс работающего dd</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">kill</span> -USR1 $(pgrep ^<span class="built_in">dd</span>$)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Проверка резервной копии</span></span><br><span class="line"><span class="built_in">md5sum</span> /dev/sda backup.img</span><br><span class="line"></span><br><span class="line"><span class="comment"># Монтирование образа резервной копии</span></span><br><span class="line"><span class="built_in">sudo</span> losetup -f -P backup.img</span><br><span class="line"><span class="built_in">sudo</span> mount /dev/loop0p1 /mnt/test</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">Полное руководство по созданию и восстановлению полных резервных копий дисков с использованием утилиты dd со сжатием и проверкой</summary>
    
    
    
    <category term="Linux" scheme="https://devapro.github.io/categories/Linux/"/>
    
    
    <category term="linux" scheme="https://devapro.github.io/tags/linux/"/>
    
    <category term="dd" scheme="https://devapro.github.io/tags/dd/"/>
    
    <category term="backup" scheme="https://devapro.github.io/tags/backup/"/>
    
    <category term="recovery" scheme="https://devapro.github.io/tags/recovery/"/>
    
  </entry>
  
</feed>
