<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rost</title>
    <description>The latest articles on DEV Community by Rost (@rosgluk).</description>
    <link>https://dev.to/rosgluk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3544400%2F04dd81bf-749e-4055-971f-316c0134e76c.jpg</url>
      <title>DEV Community: Rost</title>
      <link>https://dev.to/rosgluk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rosgluk"/>
    <language>en</language>
    <item>
      <title>Dead Letter Queues: Handling Poison Messages in Distributed Systems</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Fri, 31 Jul 2026 13:26:50 +0000</pubDate>
      <link>https://dev.to/rosgluk/dead-letter-queues-handling-poison-messages-in-distributed-systems-7p3</link>
      <guid>https://dev.to/rosgluk/dead-letter-queues-handling-poison-messages-in-distributed-systems-7p3</guid>
      <description>&lt;p&gt;A dead-letter queue is the safety net that catches messages your consumers cannot process, so one broken payload does not block or silently drop everything behind it in the queue.&lt;/p&gt;

&lt;p&gt;Every message-driven system eventually receives a message it cannot handle: a malformed payload, a schema that changed underneath the consumer, or a downstream call that fails no matter how many times you retry it. Without a dead-letter queue, that message either blocks the head of the queue forever or gets silently discarded, and both outcomes are worse than knowing about the failure.&lt;/p&gt;

&lt;p&gt;A DLQ turns an invisible failure into a visible, inspectable one. It gives you a place to quarantine the message, alert on it, and decide — deliberately, not by accident — whether to fix and replay it or discard it for good.&lt;/p&gt;

&lt;p&gt;The mechanics differ across brokers, but the underlying pattern is the same everywhere: a delivery-attempt counter, a threshold, and a destination for messages that cross it. This guide covers what a DLQ actually does, how to tell a poison message from a transient failure, when to retry versus discard, and how to replay safely once you have fixed the root cause. For the broader integration-patterns context this pattern sits inside, see the &lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;App Architecture&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Dead Letter Queue
&lt;/h2&gt;

&lt;p&gt;A dead-letter queue is a separate, ordinary queue that a broker or consumer routes a message to after that message fails processing too many times. It is not a special construct — RabbitMQ's dead-letter queue is a regular queue bound to a regular exchange, and an SQS DLQ is a regular standard or FIFO queue. What makes a queue a "DLQ" is purely that something else points failed messages at it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    P[Producer] --&amp;gt; Q[Main Queue]
    Q --&amp;gt; C[Consumer]
    C -- ack: success --&amp;gt; Done[Message deleted]
    C -- fail / nack / timeout --&amp;gt; Q
    Q -- retry budget exhausted --&amp;gt; DLQ[Dead Letter Queue]
    DLQ --&amp;gt; I[Inspect / alert]
    I -- fix root cause --&amp;gt; R[Replay to main queue]
    I -- unrecoverable --&amp;gt; D[Archive / discard]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each broker implements the redirect differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SQS&lt;/strong&gt; uses a redrive policy with a &lt;code&gt;maxReceiveCount&lt;/code&gt;. Once a message has been received that many times without being deleted, SQS moves it to the configured &lt;code&gt;deadLetterTargetArn&lt;/code&gt;. AWS explicitly recommends keeping the DLQ's message retention period longer than the source queue's, because the original enqueue timestamp — not the move time — still governs expiry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RabbitMQ&lt;/strong&gt; dead-letters a message when it is rejected with &lt;code&gt;requeue=false&lt;/code&gt;, its per-message TTL expires, the queue hits a length limit, or a quorum queue exceeds its &lt;code&gt;delivery-limit&lt;/code&gt;. You configure this with the &lt;code&gt;x-dead-letter-exchange&lt;/code&gt; (and optionally &lt;code&gt;x-dead-letter-routing-key&lt;/code&gt;) queue arguments, and RabbitMQ attaches &lt;code&gt;x-death&lt;/code&gt; headers recording the reason, the origin queue, and how many times it happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache Kafka&lt;/strong&gt; has no broker-native DLQ. Kafka only tracks offsets; it has no concept of a "failed" message. The dead-letter topic pattern is something you build in the consumer, in a Kafka Streams topology, or in a Kafka Connect connector — commonly paired with a retry-topic tier before the terminal DLT, as Spring Kafka's &lt;code&gt;@RetryableTopic&lt;/code&gt; and &lt;code&gt;DeadLetterPublishingRecoverer&lt;/code&gt; do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Service Bus&lt;/strong&gt; dead-letters automatically once a message's delivery count exceeds &lt;code&gt;MaxDeliveryCount&lt;/code&gt; (default 10), and also for a handful of system reasons such as &lt;code&gt;TTLExpiredException&lt;/code&gt;, &lt;code&gt;HeaderSizeExceeded&lt;/code&gt;, and &lt;code&gt;MaxTransferHopCountExceeded&lt;/code&gt;, each recorded in the message's &lt;code&gt;DeadLetterReason&lt;/code&gt; property.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a broader view of how brokers and streaming platforms fit together operationally rather than as a reliability pattern, &lt;a href="https://www.glukhov.org/data-infrastructure/stream-processing/apache-kafka/" rel="noopener noreferrer"&gt;Apache Kafka Quickstart&lt;/a&gt; and &lt;a href="https://www.glukhov.org/data-infrastructure/messaging/rabbitmq-on-eks-vs-sqs/" rel="noopener noreferrer"&gt;RabbitMQ on AWS EKS vs SQS&lt;/a&gt; cover the infrastructure side of running these brokers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poison Messages
&lt;/h2&gt;

&lt;p&gt;A poison message is one that will never succeed no matter how many times a consumer retries it — a malformed JSON payload, a schema field that a producer renamed, a business rule violation, or a bug that throws on a specific input every single time. That is different from a transient failure, where the message is fine but the environment briefly is not: a downstream timeout, a database connection blip, a rate limit response.&lt;/p&gt;

&lt;p&gt;Treating both failure types the same way is the most common DLQ mistake. If you dead-letter on the first failure, you punish transient errors that would have succeeded on retry. If you retry poison messages dozens of times before giving up, you waste compute, delay unrelated messages behind them (on ordered queues and partitions), and flood your logs with the same stack trace.&lt;/p&gt;

&lt;p&gt;A few detection signals help separate the two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exception type.&lt;/strong&gt; Deserialization errors, validation errors, and &lt;code&gt;ClassCastException&lt;/code&gt;-style failures are almost always permanent. Spring Kafka's &lt;code&gt;DefaultErrorHandler&lt;/code&gt; explicitly treats certain exceptions as fatal and skips retries for them rather than exhausting the retry budget first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeat count with no variance.&lt;/strong&gt; RabbitMQ's &lt;code&gt;x-death&lt;/code&gt; header array lets you see exactly how many times a message has been dead-lettered and why; a message with a growing count and an identical &lt;code&gt;x-first-death-reason&lt;/code&gt; on every cycle is poison, not unlucky.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent failure across replicas.&lt;/strong&gt; If every consumer instance fails on the same message while succeeding on everything around it, the message itself is the problem, not the infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For distinguishing retryable from non-retryable failures at the code level — the same classification a DLQ policy depends on — see &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-error-handling-architecture/" rel="noopener noreferrer"&gt;Go Error Handling Architecture: Boundaries and Patterns&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry vs Discard
&lt;/h2&gt;

&lt;p&gt;The core policy decision behind every DLQ is the retry threshold: how many delivery attempts a message gets before it is quarantined. Get this too low and you dead-letter messages that would have succeeded after a brief downstream hiccup. Get it too high and a poison message sits in the main queue for a long time, consuming worker capacity and — on ordered systems — blocking everything queued behind it.&lt;/p&gt;

&lt;p&gt;Current guidance across the major brokers converges on similar numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Broker&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Typical threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SQS&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;maxReceiveCount&lt;/code&gt; in redrive policy&lt;/td&gt;
&lt;td&gt;3–5 for mixed transient/permanent workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RabbitMQ (quorum queues)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;delivery-limit&lt;/code&gt; policy argument&lt;/td&gt;
&lt;td&gt;3–5, tuned per queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure Service Bus&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MaxDeliveryCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default 10, often reduced for latency-sensitive queues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kafka (via retry topics)&lt;/td&gt;
&lt;td&gt;Retry-count header + retry-topic tier&lt;/td&gt;
&lt;td&gt;3–4 retry-topic hops before the terminal DLT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A practical middle ground many teams land on is: start conservative (2–3 attempts), watch the actual failure mix in production, and raise the threshold only for queues where you can show most failures resolve within a few retries. Pair the retry count with &lt;strong&gt;exponential backoff and jitter&lt;/strong&gt; between attempts so a downstream outage does not turn into a retry storm — the same discipline covered in backoff and circuit-breaker design. A &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/circuit-breaker-pattern-in-go/" rel="noopener noreferrer"&gt;circuit breaker at the integration boundary&lt;/a&gt; complements this: it stops sending requests to an unhealthy dependency instead of letting every message in the queue individually discover the outage and dead-letter one by one.&lt;/p&gt;

&lt;p&gt;Once a message is in the DLQ, "discard" should still be a deliberate action, not neglect. Set a retention period on the DLQ itself — long enough to investigate (AWS recommends the DLQ retention exceed the source queue's; a week is a common floor for RabbitMQ DLQs) — and alert on DLQ depth and age so failures get triaged instead of silently expiring. A message that ages out of the DLQ unexamined is a message you decided to lose without deciding to lose it.&lt;/p&gt;

&lt;p&gt;Idempotency matters just as much here as it does anywhere else duplicates can occur: a message that gets redriven from a DLQ back to the main queue is, functionally, a duplicate delivery. If your consumer is not safe to run twice on the same message, redriving from a DLQ can create the exact duplicate-side-effect bug you were trying to avoid. See &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/idempotency-in-distributed-systems/" rel="noopener noreferrer"&gt;Idempotency in Distributed Systems That Actually Works&lt;/a&gt; for the consumer-side patterns that make redrive safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replay Strategies
&lt;/h2&gt;

&lt;p&gt;Getting a message out of the DLQ correctly is its own discipline, separate from getting it in.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fix the root cause first.&lt;/strong&gt; Deploying the consumer fix before replaying is the difference between a clean recovery and re-poisoning the queue with the same failure a second time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redrive deliberately, not automatically.&lt;/strong&gt; SQS supports a redrive-to-source feature that moves messages back to their original queue (or another destination) on demand; RabbitMQ and Kafka require you to build the equivalent consumer or tooling yourself. Either way, treat replay as an operator-triggered action with a record of what was replayed and when.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve ordering where it matters.&lt;/strong&gt; For Kafka, the dead-letter topic should have at least as many partitions as the source topic and should retain the original message key, so that replayed messages land back on the correct partition and preserve per-key ordering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap replay attempts.&lt;/strong&gt; A message that fails again after a fix-and-replay cycle is not transient — route it to a permanent archive (a database table, an object-storage bucket) instead of looping it through the DLQ indefinitely. RabbitMQ's own docs warn that a dead-lettered message can be routed between queues only a limited number of times (16) before further TTL-based dead-lettering is disabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never let a DLQ dead-letter into itself.&lt;/strong&gt; If your DLQ has its own &lt;code&gt;x-dead-letter-exchange&lt;/code&gt; (RabbitMQ) or its own redrive policy (SQS) pointed back at the same chain, a replay failure can create an infinite loop. Keep the DLQ's own dead-letter configuration empty, or point it at a strictly terminal archive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on volume, not just presence.&lt;/strong&gt; A single message in a DLQ is a data point; a sudden spike is an incident. Wire DLQ depth and message age into the same alerting pipeline you use for everything else — see &lt;a href="https://www.glukhov.org/observability/alerting/" rel="noopener noreferrer"&gt;Modern Alerting Systems Design for Observability Teams&lt;/a&gt; for routing and noise-reduction practices that apply directly to DLQ alerts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your workflow involves multi-step, long-running processes rather than single messages, the same dead-letter thinking applies at the workflow layer — a &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/saga-pattern-distributed-transactions/" rel="noopener noreferrer"&gt;saga's compensation logic&lt;/a&gt; needs the same "quarantine, inspect, decide" discipline when a step fails permanently instead of transiently. And when the events themselves originate from a database write, the &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/transactional-outbox-pattern-go/" rel="noopener noreferrer"&gt;transactional outbox pattern&lt;/a&gt; already builds dead-letter handling into the relay worker, so the pattern shows up one layer earlier than the broker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where DLQs Fit in the Bigger Picture
&lt;/h2&gt;

&lt;p&gt;A dead-letter queue does not make failures go away — it makes them survivable and reviewable instead of silent. It works best alongside retries with backoff for the transient case, idempotent consumers so redrive is safe, and a circuit breaker so a struggling dependency does not flood the main queue (and, eventually, the DLQ) with the same failure thousands of times over. Treat the DLQ threshold, retention, and alerting as first-class configuration decisions, not defaults you leave untouched, and dead letters become a diagnostic tool instead of a place where data quietly disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html" rel="noopener noreferrer"&gt;Amazon SQS Developer Guide, Dead-letter queues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rabbitmq.com/docs/dlx" rel="noopener noreferrer"&gt;RabbitMQ Documentation, Dead Letter Exchanges&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.enterprise.spring.io/spring-kafka/reference/retrytopic/dlt-strategies.html" rel="noopener noreferrer"&gt;Spring Kafka Documentation, DLT Strategies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dead-letter-queues" rel="noopener noreferrer"&gt;Microsoft Learn, Service Bus dead-letter queues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://factorhouse.io/articles/dead-letter-queues-kafka" rel="noopener noreferrer"&gt;Factor House, Dead letter queues in Kafka: patterns and pitfalls&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dev</category>
      <category>microservices</category>
    </item>
    <item>
      <title>LLM Wiki Maintenance: Drift, Contradictions and Review</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:29:50 +0000</pubDate>
      <link>https://dev.to/rosgluk/llm-wiki-maintenance-drift-contradictions-and-review-4bp1</link>
      <guid>https://dev.to/rosgluk/llm-wiki-maintenance-drift-contradictions-and-review-4bp1</guid>
      <description>&lt;p&gt;An LLM Wiki fails when old facts remain plausible, contradictions become polished, and generated summaries drift from their sources.&lt;/p&gt;

&lt;p&gt;Maintenance is the real product of any compiled knowledge system. Creating wiki pages is straightforward compared with keeping them trustworthy across months of ingest, edits, rewrites, and new sources.&lt;/p&gt;

&lt;p&gt;This article covers the operational side of LLM Wiki systems: drift detection, contradiction checks, citation discipline, linting, Git review, and maintenance workflows. It assumes you already understand the basic pattern described in &lt;a href="https://www.glukhov.org/knowledge-management/knowledge-systems-architectures/compiled-knowledge/what-is-llm-wiki/" rel="noopener noreferrer"&gt;LLM Wiki - Compiled Knowledge That RAG Cannot Replace&lt;/a&gt;: raw sources are compiled into durable Markdown pages that humans and agents can query later. The calm but opinionated view is that an LLM Wiki without maintenance is just a nicer-looking knowledge graveyard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes LLM Wiki Maintenance Different
&lt;/h2&gt;

&lt;p&gt;Traditional wikis rot because people stop updating them. RAG systems drift because the corpus changes, chunks get stale, metadata is weak, and retrieval keeps finding plausible but outdated fragments. The deeper reason is that &lt;a href="https://www.glukhov.org/knowledge-management/foundations/retrieval-vs-representation/" rel="noopener noreferrer"&gt;retrieval and representation solve different problems&lt;/a&gt;: retrieval can be re-run against fresh data, but a representation you already compiled has to be actively kept honest.&lt;/p&gt;

&lt;p&gt;An LLM Wiki has a different failure mode. It can look clean even when it is wrong.&lt;/p&gt;

&lt;p&gt;The pages may be well formatted. The links may work. The summaries may sound balanced. But underneath that neat surface, the system may have dropped critical facts, merged incompatible concepts, cited summaries instead of sources, or preserved an old decision as if it still applied.&lt;/p&gt;

&lt;p&gt;That is why LLM Wiki maintenance must check both structure and meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Maintenance Goal
&lt;/h2&gt;

&lt;p&gt;The goal is not to make every page perfect. That is too expensive, and it usually leads to abandoned systems.&lt;/p&gt;

&lt;p&gt;The goal is to keep the wiki useful, inspectable, and recoverable.&lt;/p&gt;

&lt;p&gt;A maintained LLM Wiki should make it easy to answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What sources support this claim?&lt;/li&gt;
&lt;li&gt;When was this page last reviewed?&lt;/li&gt;
&lt;li&gt;Has this concept changed?&lt;/li&gt;
&lt;li&gt;Are there conflicting pages?&lt;/li&gt;
&lt;li&gt;Is this summary still current?&lt;/li&gt;
&lt;li&gt;Did the agent rewrite more than it should?&lt;/li&gt;
&lt;li&gt;Can we roll back a bad update?&lt;/li&gt;
&lt;li&gt;Can a human understand why the page says what it says?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is no, the problem is not only content quality. It is system design.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Maintenance Loop
&lt;/h2&gt;

&lt;p&gt;A useful LLM Wiki needs a repeatable loop. The loop should be simple enough to run often and strict enough to catch drift before it becomes invisible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Add or update source] --&amp;gt; B[Compile into wiki pages]
    B --&amp;gt; C[Update links and indexes]
    C --&amp;gt; D[Run structural lint checks]
    D --&amp;gt; E[Run semantic review checks]
    E --&amp;gt; F[Human review of risky changes]
    F --&amp;gt; G[Commit approved changes]
    G --&amp;gt; H[Schedule stale-page review]
    H --&amp;gt; A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This loop is not glamorous. That is the point.&lt;/p&gt;

&lt;p&gt;A knowledge system becomes durable through boring maintenance: source preservation, explicit review, predictable structure, and small safe updates. The same instinct drives &lt;a href="https://www.glukhov.org/knowledge-management/methods/evergreen-notes/" rel="noopener noreferrer"&gt;evergreen notes&lt;/a&gt;: a note or a wiki page only compounds in value if someone keeps refining it instead of leaving it to rot after the first draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of Drift in an LLM Wiki
&lt;/h2&gt;

&lt;p&gt;Knowledge drift is not one thing. Different drift types require different checks. A good maintenance system should name them clearly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Source Drift
&lt;/h3&gt;

&lt;p&gt;Source drift happens when the underlying source material changes.&lt;/p&gt;

&lt;p&gt;For example, a tool releases a new version, a policy is updated, an API changes, or a vendor deprecates an old feature. The old wiki page may still be accurate for the previous version, but wrong for current use.&lt;/p&gt;

&lt;p&gt;Source drift is dangerous because the old claim may still be true in historical context. The problem is not that the claim is fake. The problem is that it no longer answers the current question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Record source dates&lt;/li&gt;
&lt;li&gt;Record last reviewed dates&lt;/li&gt;
&lt;li&gt;Mark version-specific pages clearly&lt;/li&gt;
&lt;li&gt;Link old pages to superseding pages&lt;/li&gt;
&lt;li&gt;Avoid mixing old and new versions without labels&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Concept Drift
&lt;/h3&gt;

&lt;p&gt;Concept drift happens when the meaning of a term changes over time.&lt;/p&gt;

&lt;p&gt;This is common in AI and software architecture. Terms like "agent", "memory", "RAG", "workflow", "tool use", "structured output", and "context engineering" can shift meaning quickly.&lt;/p&gt;

&lt;p&gt;A wiki can accidentally preserve several meanings of the same term without explaining the difference. That creates confusing pages that sound coherent but combine incompatible ideas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintain glossary pages&lt;/li&gt;
&lt;li&gt;Add "meaning in this wiki" sections&lt;/li&gt;
&lt;li&gt;Separate overloaded concepts into distinct pages&lt;/li&gt;
&lt;li&gt;Link related meanings explicitly&lt;/li&gt;
&lt;li&gt;Avoid letting the model silently merge terms&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Terminology Drift
&lt;/h3&gt;

&lt;p&gt;Terminology drift is smaller than concept drift but still harmful.&lt;/p&gt;

&lt;p&gt;It happens when the wiki uses multiple names for the same thing: "LLM Wiki", "compiled knowledge base", "AI-maintained wiki", "agent-maintained wiki", and "Markdown knowledge base".&lt;/p&gt;

&lt;p&gt;Some variation is fine. Too much variation breaks search, linking, and review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define canonical page names&lt;/li&gt;
&lt;li&gt;Keep aliases in front matter or page metadata&lt;/li&gt;
&lt;li&gt;Redirect duplicate pages&lt;/li&gt;
&lt;li&gt;Lint for near-duplicate titles&lt;/li&gt;
&lt;li&gt;Use consistent anchor text in internal links&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Decision Drift
&lt;/h3&gt;

&lt;p&gt;Decision drift happens when a past decision remains documented but no longer reflects current practice.&lt;/p&gt;

&lt;p&gt;For example, a page might say that the project uses vector RAG for all document search, while newer pages describe an LLM Wiki workflow. Both statements may be historically true, but the wiki must show which one is current.&lt;/p&gt;

&lt;p&gt;This matters for architecture notes, engineering processes, content strategy, and internal tooling. The same discipline applies to &lt;a href="https://www.glukhov.org/app-architecture/documentation/decision-records-ai-driven-development/" rel="noopener noreferrer"&gt;decision records in AI-driven development&lt;/a&gt;: a decision page is only trustworthy if superseded choices are marked as superseded rather than silently overwritten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mark decisions as proposed, accepted, superseded, or rejected&lt;/li&gt;
&lt;li&gt;Keep decision dates&lt;/li&gt;
&lt;li&gt;Link superseded decisions to replacements&lt;/li&gt;
&lt;li&gt;Preserve historical context&lt;/li&gt;
&lt;li&gt;Avoid deleting old decisions without trace&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Citation Drift
&lt;/h3&gt;

&lt;p&gt;Citation drift happens when a page cites a source, but the claim no longer matches what the source says.&lt;/p&gt;

&lt;p&gt;This can happen after a rewrite. The citation remains in place, but the sentence around it changes. The page still looks sourced, yet the citation no longer supports the claim.&lt;/p&gt;

&lt;p&gt;This is one of the most serious LLM Wiki failure modes because it creates false confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check claim-level citation support&lt;/li&gt;
&lt;li&gt;Avoid citing only summaries&lt;/li&gt;
&lt;li&gt;Keep raw sources&lt;/li&gt;
&lt;li&gt;Require citations for important claims&lt;/li&gt;
&lt;li&gt;Flag paragraphs with sources but no direct support&lt;/li&gt;
&lt;li&gt;Review citation changes in Git diffs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Structure Drift
&lt;/h3&gt;

&lt;p&gt;Structure drift happens when the wiki slowly loses navigability.&lt;/p&gt;

&lt;p&gt;New pages are created instead of updating old ones. Index pages fall behind. Duplicate pages appear. Orphan pages accumulate. Related pages stop linking to each other.&lt;/p&gt;

&lt;p&gt;The wiki still contains useful knowledge, but finding and trusting it becomes harder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance response:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lint for orphan pages&lt;/li&gt;
&lt;li&gt;Maintain index pages&lt;/li&gt;
&lt;li&gt;Detect duplicate topics&lt;/li&gt;
&lt;li&gt;Require backlinks for canonical pages&lt;/li&gt;
&lt;li&gt;Archive rather than scatter&lt;/li&gt;
&lt;li&gt;Keep folder roles clear&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Core Maintenance Files
&lt;/h2&gt;

&lt;p&gt;An LLM Wiki should not rely on memory, vibes, or repeated prompting. It needs operating files that tell humans and agents how to maintain it.&lt;/p&gt;

&lt;p&gt;A practical structure can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;llm-wiki/
  raw/
    sources/
    transcripts/
    documents/
  wiki/
    index.md
    concepts/
    entities/
    projects/
    decisions/
  maintenance/
    review-log.md
    lint-report.md
    stale-pages.md
    contradiction-report.md
  rules/
    AGENTS.md
    schema.md
    style-guide.md
    citation-policy.md
    source-policy.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact names do not matter. The roles do.&lt;/p&gt;

&lt;p&gt;The wiki needs source storage, compiled pages, review artifacts, and rules that survive across sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  AGENTS.md
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; should explain how the agent is allowed to work.&lt;/p&gt;

&lt;p&gt;It should answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should the agent update existing pages before creating new ones?&lt;/li&gt;
&lt;li&gt;When should it ask for review?&lt;/li&gt;
&lt;li&gt;How should it cite sources?&lt;/li&gt;
&lt;li&gt;Can it reorganize folders?&lt;/li&gt;
&lt;li&gt;Can it rewrite old pages?&lt;/li&gt;
&lt;li&gt;What should it do with contradictions?&lt;/li&gt;
&lt;li&gt;What should it never delete?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prefer small, source-backed updates over broad rewrites.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one sentence prevents many bad maintenance habits. It also draws a useful boundary relative to &lt;a href="https://www.glukhov.org/ai-systems/memory/memory-systems-in-ai-assistants/" rel="noopener noreferrer"&gt;agent memory&lt;/a&gt;: memory shapes how an agent behaves in the moment, while the wiki's rules file shapes what the agent is allowed to change in the shared knowledge base.&lt;/p&gt;

&lt;h3&gt;
  
  
  schema.md
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;schema.md&lt;/code&gt; should define the page structure.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Page Title

## Summary
Short current summary.

## Key Claims
- Claim with source reference.
- Claim with source reference.

## Current Status
Current, historical, superseded, draft, or uncertain.

## Details
Main compiled explanation.

## Related Pages
Internal links.

## Sources
Raw source references.

## Review Notes
Last reviewed date and reviewer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A schema does not need to be rigid. But without a schema, the wiki becomes a pile of inconsistent essays. The schema gives the agent something to preserve.&lt;/p&gt;

&lt;h3&gt;
  
  
  citation-policy.md
&lt;/h3&gt;

&lt;p&gt;The citation policy should say what requires a source. At minimum, require citations for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Technical claims&lt;/li&gt;
&lt;li&gt;Comparisons&lt;/li&gt;
&lt;li&gt;Product behavior&lt;/li&gt;
&lt;li&gt;Version-specific statements&lt;/li&gt;
&lt;li&gt;Benchmark results&lt;/li&gt;
&lt;li&gt;Pricing&lt;/li&gt;
&lt;li&gt;Legal or compliance claims&lt;/li&gt;
&lt;li&gt;Current status claims&lt;/li&gt;
&lt;li&gt;Claims copied or derived from a source&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good citation policy also says what not to do. Do not cite a compiled page as if it were the original source. Do not attach citations to paragraphs they do not support. Do not keep a citation after rewriting a claim unless the source still supports it.&lt;/p&gt;

&lt;p&gt;Citation discipline is the difference between a useful LLM Wiki and decorative confidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  source-policy.md
&lt;/h3&gt;

&lt;p&gt;The source policy should define how raw sources are stored.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep original files when possible&lt;/li&gt;
&lt;li&gt;Preserve URLs and access dates&lt;/li&gt;
&lt;li&gt;Store copied notes separately from generated summaries&lt;/li&gt;
&lt;li&gt;Record source type&lt;/li&gt;
&lt;li&gt;Mark low-confidence sources&lt;/li&gt;
&lt;li&gt;Avoid overwriting raw material&lt;/li&gt;
&lt;li&gt;Keep superseded sources when they explain historical decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This policy protects the wiki from a common mistake: summarizing a source, discarding the original, and then treating the summary as evidence.&lt;/p&gt;

&lt;p&gt;That is not knowledge management. That is lossy compression.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an LLM Wiki Linter Should Check
&lt;/h2&gt;

&lt;p&gt;A linter should check structure first. Structural problems are easier to automate and often reveal deeper semantic problems.&lt;/p&gt;

&lt;p&gt;Useful structural checks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Broken internal links&lt;/li&gt;
&lt;li&gt;Orphan pages&lt;/li&gt;
&lt;li&gt;Duplicate titles&lt;/li&gt;
&lt;li&gt;Missing source sections&lt;/li&gt;
&lt;li&gt;Missing review dates&lt;/li&gt;
&lt;li&gt;Pages without backlinks&lt;/li&gt;
&lt;li&gt;Empty or placeholder sections&lt;/li&gt;
&lt;li&gt;Very long pages without section structure&lt;/li&gt;
&lt;li&gt;Pages with no incoming links from an index&lt;/li&gt;
&lt;li&gt;Inconsistent naming conventions&lt;/li&gt;
&lt;li&gt;Invalid front matter&lt;/li&gt;
&lt;li&gt;Stale "current" pages older than a review threshold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks do not prove the knowledge is true. They prove the wiki is still maintainable. That is a necessary starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semantic Checks
&lt;/h2&gt;

&lt;p&gt;Semantic checks are harder but more valuable.&lt;/p&gt;

&lt;p&gt;Useful semantic checks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claims without direct source support&lt;/li&gt;
&lt;li&gt;Two pages making incompatible claims&lt;/li&gt;
&lt;li&gt;Old decisions presented as current&lt;/li&gt;
&lt;li&gt;Duplicate concepts with different names&lt;/li&gt;
&lt;li&gt;One concept page mixing several meanings&lt;/li&gt;
&lt;li&gt;Summaries that omit known constraints&lt;/li&gt;
&lt;li&gt;"Best" or "recommended" claims without criteria&lt;/li&gt;
&lt;li&gt;Version-specific claims without version labels&lt;/li&gt;
&lt;li&gt;Pages that contradict newer sources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks should not automatically rewrite the wiki. They should usually produce a report for review.&lt;/p&gt;

&lt;p&gt;The safer pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Detect automatically.
Explain clearly.
Update deliberately.
Review risky changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Contradiction Detection
&lt;/h2&gt;

&lt;p&gt;Contradiction detection is not just asking an LLM whether two pages contradict each other.&lt;/p&gt;

&lt;p&gt;That can help, but it is too vague. A better approach is to compare claims.&lt;/p&gt;

&lt;p&gt;A contradiction workflow can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Extract claims from page] --&amp;gt; B[Find related pages and sources]
    B --&amp;gt; C[Extract claims from related material]
    C --&amp;gt; D[Group claims by subject]
    D --&amp;gt; E[Compare status, date, version, and scope]
    E --&amp;gt; F{Conflict found?}
    F --&amp;gt;|No| G[No action]
    F --&amp;gt;|Yes| H[Classify conflict]
    H --&amp;gt; I[Create contradiction report]
    I --&amp;gt; J[Human or agent-assisted resolution]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works better because many apparent contradictions are not real contradictions.&lt;/p&gt;

&lt;p&gt;One page may describe version 1.0 and another version 2.0. One page may describe personal use and another enterprise use. One page may describe the design goal and another the implementation reality.&lt;/p&gt;

&lt;p&gt;The contradiction report should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conflicting claims&lt;/li&gt;
&lt;li&gt;Pages involved&lt;/li&gt;
&lt;li&gt;Source references&lt;/li&gt;
&lt;li&gt;Date or version context&lt;/li&gt;
&lt;li&gt;Likely explanation&lt;/li&gt;
&lt;li&gt;Suggested resolution&lt;/li&gt;
&lt;li&gt;Whether human review is required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not let the agent silently resolve contradictions by blending both claims into a vague compromise. That creates smooth nonsense.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Resolve Contradictions
&lt;/h2&gt;

&lt;p&gt;There are several valid ways to resolve a contradiction.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If one claim is outdated, mark it as superseded and link to the newer page.&lt;/li&gt;
&lt;li&gt;If both claims are true in different contexts, split the context clearly.&lt;/li&gt;
&lt;li&gt;If the sources disagree, preserve the disagreement and explain it.&lt;/li&gt;
&lt;li&gt;If the wiki page invented or overgeneralized a claim, remove or narrow the claim.&lt;/li&gt;
&lt;li&gt;If the contradiction reflects a real unresolved decision, create or update a decision page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to remove all tension. Some tension is useful. A good wiki shows where knowledge is settled and where it is still uncertain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stale Page Review
&lt;/h2&gt;

&lt;p&gt;Every compiled page should have a review status.&lt;/p&gt;

&lt;p&gt;A simple metadata block can be enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;current&lt;/span&gt;
&lt;span class="na"&gt;last_reviewed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-07-10&lt;/span&gt;
&lt;span class="na"&gt;review_after&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-10-10&lt;/span&gt;
&lt;span class="na"&gt;source_confidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;medium&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For fast-moving topics, use short review windows. For stable concepts, use longer windows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page type&lt;/th&gt;
&lt;th&gt;Review interval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tool version pages&lt;/td&gt;
&lt;td&gt;30 to 90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing or availability pages&lt;/td&gt;
&lt;td&gt;7 to 30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture principles&lt;/td&gt;
&lt;td&gt;6 to 18 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical decision records&lt;/td&gt;
&lt;td&gt;Only when superseded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glossary pages&lt;/td&gt;
&lt;td&gt;3 to 12 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source summaries&lt;/td&gt;
&lt;td&gt;When source changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comparison pages&lt;/td&gt;
&lt;td&gt;30 to 180 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The interval is less important than the habit. A page without a review date is a page that will eventually lie quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source-Aware Updates
&lt;/h2&gt;

&lt;p&gt;When a new source arrives, the agent should not simply write a new summary page.&lt;/p&gt;

&lt;p&gt;It should ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this update an existing concept?&lt;/li&gt;
&lt;li&gt;Does it supersede a previous source?&lt;/li&gt;
&lt;li&gt;Does it contradict an existing page?&lt;/li&gt;
&lt;li&gt;Does it add a new entity or term?&lt;/li&gt;
&lt;li&gt;Does it change a recommendation?&lt;/li&gt;
&lt;li&gt;Does it require an index update?&lt;/li&gt;
&lt;li&gt;Does it affect decision pages?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good ingest workflow updates the wiki as a graph, not as a stack of isolated summaries.&lt;/p&gt;

&lt;p&gt;This is why one source may touch many pages. That is normal. The maintenance challenge is making those updates small, reviewable, and traceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review with Git Diffs
&lt;/h2&gt;

&lt;p&gt;Git is one of the best maintenance tools for an LLM Wiki.&lt;/p&gt;

&lt;p&gt;Not because Git is fashionable, but because generated knowledge needs reviewable change history. A Git diff shows what the agent changed, deleted, moved, or rephrased.&lt;/p&gt;

&lt;p&gt;Use Git to review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New pages&lt;/li&gt;
&lt;li&gt;Deleted sections&lt;/li&gt;
&lt;li&gt;Changed claims&lt;/li&gt;
&lt;li&gt;Changed citations&lt;/li&gt;
&lt;li&gt;Renamed pages&lt;/li&gt;
&lt;li&gt;Index updates&lt;/li&gt;
&lt;li&gt;Link changes&lt;/li&gt;
&lt;li&gt;Status changes&lt;/li&gt;
&lt;li&gt;Broad rewrites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important review habit is to inspect deletions. LLMs often remove details while making prose cleaner.&lt;/p&gt;

&lt;p&gt;Clean prose is not always better knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe Commit Strategy
&lt;/h2&gt;

&lt;p&gt;Avoid giant commits like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update wiki
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use commits that explain the operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ingest: add source notes for Qwen embedding release
maint: mark old vector store comparison as superseded
lint: fix broken internal links in RAG pages
review: update LLM Wiki page citations
refactor: split agent memory concept from long-term memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives future humans and agents a useful history.&lt;/p&gt;

&lt;p&gt;Commit messages are part of the knowledge system. They explain why the wiki changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review Levels
&lt;/h2&gt;

&lt;p&gt;Not every page needs the same review level. Use risk-based review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low-risk changes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Broken link fixes&lt;/li&gt;
&lt;li&gt;Formatting cleanup&lt;/li&gt;
&lt;li&gt;Adding backlinks&lt;/li&gt;
&lt;li&gt;Adding index entries&lt;/li&gt;
&lt;li&gt;Correcting typos&lt;/li&gt;
&lt;li&gt;Adding aliases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Medium-risk changes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding source summaries&lt;/li&gt;
&lt;li&gt;Creating new concept pages&lt;/li&gt;
&lt;li&gt;Updating recommendations&lt;/li&gt;
&lt;li&gt;Merging duplicate pages&lt;/li&gt;
&lt;li&gt;Changing page status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;High-risk changes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deleting claims&lt;/li&gt;
&lt;li&gt;Rewriting canonical pages&lt;/li&gt;
&lt;li&gt;Changing decision records&lt;/li&gt;
&lt;li&gt;Updating security, legal, pricing, or benchmark claims&lt;/li&gt;
&lt;li&gt;Resolving contradictions&lt;/li&gt;
&lt;li&gt;Marking content as current or superseded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;High-risk changes should get human review. That may be you, a maintainer, or a domain owner.&lt;/p&gt;

&lt;p&gt;The agent can prepare the change, but it should not always be the final authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Maintenance Dashboard
&lt;/h2&gt;

&lt;p&gt;A simple maintenance dashboard can live in Markdown.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# LLM Wiki Maintenance Dashboard

## Needs Review
- wiki/concepts/agent-memory.md - review overdue
- wiki/projects/search-index.md - conflicting claims found

## Stale Pages
- wiki/tools/ollama.md - last reviewed 120 days ago
- wiki/concepts/rag.md - fast-moving topic, review due

## Broken Links
- wiki/index.md -&amp;gt; wiki/concepts/old-page.md

## Orphan Pages
- wiki/entities/vendor-x.md

## Contradiction Reports
- reports/contradictions/2026-07-10-agent-memory.md

## Recent Ingests
- raw/sources/2026-07-09-new-paper.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a fancy admin panel. It is a working surface.&lt;/p&gt;

&lt;p&gt;For many solo or small-team systems, a Markdown dashboard is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metrics for a Healthy LLM Wiki
&lt;/h2&gt;

&lt;p&gt;You can track wiki health without overengineering it.&lt;/p&gt;

&lt;p&gt;Useful metrics include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of pages&lt;/li&gt;
&lt;li&gt;Pages without sources&lt;/li&gt;
&lt;li&gt;Pages without review dates&lt;/li&gt;
&lt;li&gt;Pages past review date&lt;/li&gt;
&lt;li&gt;Broken internal links&lt;/li&gt;
&lt;li&gt;Orphan pages&lt;/li&gt;
&lt;li&gt;Duplicate titles or aliases&lt;/li&gt;
&lt;li&gt;Contradiction reports open&lt;/li&gt;
&lt;li&gt;Contradiction reports resolved&lt;/li&gt;
&lt;li&gt;Average page age by cluster&lt;/li&gt;
&lt;li&gt;Raw sources not yet ingested&lt;/li&gt;
&lt;li&gt;Pages changed without human review&lt;/li&gt;
&lt;li&gt;Canonical pages changed in the last 30 days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These metrics are not vanity numbers. They tell you whether the system is becoming easier or harder to trust.&lt;/p&gt;

&lt;p&gt;A growing wiki with rising orphan pages and stale reviews is not compounding knowledge. It is compounding maintenance debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance Prompts
&lt;/h2&gt;

&lt;p&gt;Maintenance prompts should be narrow. Broad prompts produce broad rewrites.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structural Lint Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review the wiki structure.

Check for:
- broken internal links
- orphan pages
- duplicate or near-duplicate page titles
- pages missing source sections
- pages missing last_reviewed metadata
- index pages that do not link to new pages

Do not rewrite content.
Produce a Markdown report with findings and suggested fixes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Citation Review Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review citations for the selected page.

For each important claim:
- identify the supporting source
- check whether the source directly supports the claim
- flag claims with weak, missing, or mismatched support
- do not rewrite the page yet

Return a table with:
claim, current citation, support level, issue, suggested action.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Contradiction Check Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check this page for contradictions against related pages.

Steps:
1. Extract the main claims from the page.
2. Find related pages through links, backlinks, aliases, and search.
3. Extract potentially conflicting claims.
4. Classify each conflict as:
   - real contradiction
   - version difference
   - scope difference
   - terminology difference
   - unresolved uncertainty
5. Recommend a resolution.

Do not edit files until the contradiction report is reviewed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stale Page Review Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review this page for staleness.

Check:
- dates
- version-specific claims
- tool behavior
- recommendations
- links to superseded pages
- sources newer than the page review date
- claims marked current without recent support

Return:
- keep current
- update needed
- mark historical
- mark superseded
- split into versioned pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Safe Update Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update the selected wiki page using the provided source.

Rules:
- preserve existing useful structure
- do not remove sourced claims unless the source is superseded
- add citations for new claims
- mark uncertainty explicitly
- update related pages only when necessary
- add a review note explaining what changed
- keep the diff small
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These prompts are not magic. They are guardrails. The real value comes from making the maintenance action explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Maintenance Mistakes
&lt;/h2&gt;

&lt;p&gt;The first mistake is letting the agent rewrite too much.&lt;/p&gt;

&lt;p&gt;Large rewrites feel productive because they make pages smoother. They also make it harder to see what changed and easier to lose sharp details.&lt;/p&gt;

&lt;p&gt;The second mistake is treating generated summaries as sources.&lt;/p&gt;

&lt;p&gt;A summary can be useful, but it is not the evidence. Keep raw sources and cite them when claims matter.&lt;/p&gt;

&lt;p&gt;The third mistake is creating new pages instead of updating old ones.&lt;/p&gt;

&lt;p&gt;This produces duplicate concepts, conflicting recommendations, and search confusion. The agent should search first, update second, and create new pages only when the concept is genuinely new.&lt;/p&gt;

&lt;p&gt;The fourth mistake is deleting historical context.&lt;/p&gt;

&lt;p&gt;Old decisions, failed experiments, and superseded recommendations can be valuable. Mark them clearly instead of erasing them.&lt;/p&gt;

&lt;p&gt;The fifth mistake is forgetting indexes.&lt;/p&gt;

&lt;p&gt;A page that is not linked from the right place is half-lost. Index maintenance is not clerical work. It is navigation architecture.&lt;/p&gt;

&lt;p&gt;The sixth mistake is using the same review policy for every page.&lt;/p&gt;

&lt;p&gt;A stable conceptual page and a fast-moving tool comparison should not have the same review schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Archive, Supersede, or Delete
&lt;/h2&gt;

&lt;p&gt;Most stale pages should not be deleted immediately.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Archive&lt;/td&gt;
&lt;td&gt;The page is historical but still useful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supersede&lt;/td&gt;
&lt;td&gt;A newer page replaces the old answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;The page is duplicate, empty, wrong, or unrecoverable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A superseded page should link to the replacement.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status: superseded
Superseded by: wiki/concepts/agent-memory-architecture.md
Reason: This page used an older definition of agent memory before the project separated session memory, user memory, and compiled knowledge.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preserves context and reduces confusion.&lt;/p&gt;

&lt;p&gt;Deletion should be rare and reviewable. Knowledge systems need pruning, but invisible pruning is dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canonical Pages Need Extra Care
&lt;/h2&gt;

&lt;p&gt;Every LLM Wiki eventually develops canonical pages.&lt;/p&gt;

&lt;p&gt;These are pages that define core concepts, architecture choices, workflows, or project vocabulary. They are more important than ordinary notes because many other pages depend on them.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is LLM Wiki&lt;/li&gt;
&lt;li&gt;RAG vs compiled knowledge&lt;/li&gt;
&lt;li&gt;Agent memory&lt;/li&gt;
&lt;li&gt;Source policy&lt;/li&gt;
&lt;li&gt;Review workflow&lt;/li&gt;
&lt;li&gt;Project architecture&lt;/li&gt;
&lt;li&gt;Glossary&lt;/li&gt;
&lt;li&gt;Index&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changes to canonical pages should require stricter review.&lt;/p&gt;

&lt;p&gt;A small error on an obscure page is local. A small error on a canonical page can distort the whole wiki.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Often Should You Run Maintenance
&lt;/h2&gt;

&lt;p&gt;For an active LLM Wiki, run small maintenance frequently and deep maintenance occasionally.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;th&gt;Maintenance task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Every ingest&lt;/td&gt;
&lt;td&gt;Update links, sources, indexes, and review notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;td&gt;Run structural lint checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;Review stale pages and orphan pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;Check contradictions in active clusters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;Review canonical pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;Archive or supersede old pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Before major use&lt;/td&gt;
&lt;td&gt;Run citation checks on pages used for reports, articles, or decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The schedule should match risk. A personal research wiki can be lighter. A wiki used for technical publishing, customer support, or engineering decisions needs more discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using an LLM Wiki for Technical Publishing
&lt;/h2&gt;

&lt;p&gt;For a technical blog or documentation site, the LLM Wiki can become the knowledge layer behind article planning.&lt;/p&gt;

&lt;p&gt;It can track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Canonical explanations&lt;/li&gt;
&lt;li&gt;Repeated definitions&lt;/li&gt;
&lt;li&gt;Internal link opportunities&lt;/li&gt;
&lt;li&gt;Stale comparisons&lt;/li&gt;
&lt;li&gt;Article gaps&lt;/li&gt;
&lt;li&gt;Topic clusters&lt;/li&gt;
&lt;li&gt;Source notes&lt;/li&gt;
&lt;li&gt;Decisions about structure&lt;/li&gt;
&lt;li&gt;Claims that need verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is useful, but it raises the maintenance bar.&lt;/p&gt;

&lt;p&gt;If the wiki feeds published articles, then stale wiki pages can become stale public content. If the wiki contains contradictions, those contradictions may leak into articles. If citations are weak, generated drafts will inherit that weakness. The scoped summaries, schema-based extraction, and human review loops described in &lt;a href="https://www.glukhov.org/knowledge-management/ai-augmented-knowledge/ai-for-knowledge-management-workflows/" rel="noopener noreferrer"&gt;AI for Knowledge Management: Real Workflows That Hold Up&lt;/a&gt; are a good template for keeping that editorial pipeline honest.&lt;/p&gt;

&lt;p&gt;For publishing workflows, treat the LLM Wiki as an editorial system, not just a private notebook. A &lt;a href="https://www.glukhov.org/knowledge-management/methods/digital-gardening/" rel="noopener noreferrer"&gt;digital garden&lt;/a&gt; makes this explicit by showing readers which pages are still growing and which are mature, which is exactly the kind of status signal a well-maintained LLM Wiki should also expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance Is Not Optional
&lt;/h2&gt;

&lt;p&gt;An LLM Wiki is attractive because it promises compounding knowledge.&lt;/p&gt;

&lt;p&gt;But compounding only works if old knowledge remains useful. Otherwise, the system compounds noise, not insight.&lt;/p&gt;

&lt;p&gt;The maintenance burden does not disappear because an agent is involved. It changes shape.&lt;/p&gt;

&lt;p&gt;Humans should not have to do every tedious task. Agents can check links, find stale pages, compare claims, draft reports, and propose updates. But humans still need to set policy, review risky changes, and decide what the wiki is allowed to mean.&lt;/p&gt;

&lt;p&gt;That is the real bargain.&lt;/p&gt;

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

&lt;p&gt;LLM Wiki maintenance is about keeping compiled knowledge honest.&lt;/p&gt;

&lt;p&gt;The important practices are not complicated: preserve sources, cite claims, keep indexes current, detect contradictions, review Git diffs, mark stale pages, and avoid broad unreviewed rewrites.&lt;/p&gt;

&lt;p&gt;A good LLM Wiki does not pretend to be magically self-correcting. It makes correction easier.&lt;/p&gt;

&lt;p&gt;That is the standard worth aiming for: not perfect knowledge, but inspectable knowledge that can be reviewed, repaired, and trusted over time.&lt;/p&gt;

</description>
      <category>knowledgemanagement</category>
      <category>wiki</category>
      <category>llm</category>
      <category>documentation</category>
    </item>
    <item>
      <title>Syncthing File Sync for Self-Hosted Knowledge Systems</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sun, 19 Jul 2026 12:19:06 +0000</pubDate>
      <link>https://dev.to/rosgluk/syncthing-file-sync-for-self-hosted-knowledge-systems-44e7</link>
      <guid>https://dev.to/rosgluk/syncthing-file-sync-for-self-hosted-knowledge-systems-44e7</guid>
      <description>&lt;p&gt;Syncthing keeps files synchronized across devices you control, making it one of the most practical tools for a self-hosted knowledge infrastructure that avoids cloud lock-in.&lt;/p&gt;

&lt;p&gt;Unlike cloud storage platforms, Syncthing uses a peer-to-peer model where each device holds its own copy of synced folders and exchanges changes directly with trusted peers. There is no central server that owns your data, no subscription account, and no vendor lock-in. The project is open-source and community-driven, with more details at &lt;a href="https://syncthing.net" rel="noopener noreferrer"&gt;syncthing.net&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This architecture makes Syncthing especially useful for knowledge workers who manage markdown notes, research documents, PDFs, and project files across a desktop, laptop, home server, and possibly a phone. The tool is simple in concept but requires careful setup to avoid common pitfalls like treating sync as backup or syncing folders that should remain isolated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Syncthing Is and Is Not
&lt;/h2&gt;

&lt;p&gt;Syncthing synchronizes files between two or more devices. Each device maintains its own copy of a folder, and changes propagate between trusted peers. Discovery and relay services may help devices find each other across networks, but the storage model remains local-first. The &lt;a href="https://docs.syncthing.net" rel="noopener noreferrer"&gt;Syncthing documentation&lt;/a&gt; covers installation and configuration in detail.&lt;/p&gt;

&lt;p&gt;The calm but important opinion is this: Syncthing is excellent when treated as sync infrastructure. It becomes dangerous when treated as backup.&lt;/p&gt;

&lt;p&gt;It is not a cloud drive. It is not a complete backup system. It is not a collaboration suite. It is a private, peer-to-peer file synchronization tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Syncthing Matters for Knowledge Management
&lt;/h2&gt;

&lt;p&gt;Knowledge management is not only about note-taking. It is also about where knowledge lives, how it moves, and whether it remains accessible over time — see the &lt;a href="https://www.glukhov.org/knowledge-management/" rel="noopener noreferrer"&gt;knowledge management guide&lt;/a&gt; for the broader picture of tools, methods, and self-hosted platforms this fits into.&lt;/p&gt;

&lt;p&gt;A useful personal or team knowledge system often contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;markdown notes&lt;/li&gt;
&lt;li&gt;PDFs and papers&lt;/li&gt;
&lt;li&gt;diagrams and screenshots&lt;/li&gt;
&lt;li&gt;exported web pages&lt;/li&gt;
&lt;li&gt;source snippets and configuration files&lt;/li&gt;
&lt;li&gt;meeting notes and project documents&lt;/li&gt;
&lt;li&gt;scanned documents and plain text logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many of these are just files. That is good. Files are durable, portable, searchable, scriptable, and easy to back up.&lt;/p&gt;

&lt;p&gt;Syncthing gives those files movement without forcing them into a vendor platform. You can write notes on one machine, read them on another, keep a copy on a home server, and still use normal tools like &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;ripgrep&lt;/code&gt;, &lt;a href="https://www.glukhov.org/knowledge-management/tools/obsidian-for-personal-knowledge-management/" rel="noopener noreferrer"&gt;Obsidian&lt;/a&gt;, VS Code, DokuWiki imports, static site generators, or custom scripts.&lt;/p&gt;

&lt;p&gt;For a self-hosted knowledge system, that is a strong architectural property. See &lt;a href="https://www.glukhov.org/knowledge-management/foundations/personal-knowledge-management/" rel="noopener noreferrer"&gt;personal knowledge management foundations&lt;/a&gt; for a broader view of PKM goals and methods, and &lt;a href="https://www.glukhov.org/knowledge-management/foundations/pkm-vs-rag-vs-wiki-vs-memory-systems/" rel="noopener noreferrer"&gt;PKM vs RAG vs Wiki vs Memory Systems&lt;/a&gt; for how different knowledge systems operate at different layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinct System Roles
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph LR
    S["Syncthing&amp;lt;br/&amp;gt;moves files"] --&amp;gt; B["Backup&amp;lt;br/&amp;gt;preserves history"]
    S --&amp;gt; K["Knowledge tools&amp;lt;br/&amp;gt;create and edit"]
    B --&amp;gt; R["Recovery&amp;lt;br/&amp;gt;when things go wrong"]
    K --&amp;gt; S
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep those roles separate.&lt;/p&gt;

&lt;p&gt;Syncthing should not be your only copy of important data. It should not be the only thing standing between you and accidental deletion. It should not be the only recovery mechanism after corruption, ransomware, filesystem failure, or a bad script.&lt;/p&gt;

&lt;p&gt;Used correctly, Syncthing is part of a resilient file workflow. Used alone, it can replicate mistakes very efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good Use Cases for Syncthing
&lt;/h2&gt;

&lt;p&gt;Syncthing works best when the folder has a clear owner, a predictable structure, and a small number of trusted devices.&lt;/p&gt;

&lt;p&gt;Good use cases include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syncing an Obsidian or markdown notes vault&lt;/li&gt;
&lt;li&gt;Syncing project notes between desktop and laptop&lt;/li&gt;
&lt;li&gt;Syncing documents to an always-on home server&lt;/li&gt;
&lt;li&gt;Syncing exported PDFs and research material&lt;/li&gt;
&lt;li&gt;Syncing configuration files across personal machines&lt;/li&gt;
&lt;li&gt;Syncing scanned documents from one machine to another&lt;/li&gt;
&lt;li&gt;Syncing selected phone folders to a desktop or NAS&lt;/li&gt;
&lt;li&gt;Syncing static-site source notes before publishing&lt;/li&gt;
&lt;li&gt;Syncing knowledge archives between a workstation and server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not exotic use cases. They are exactly the kind of boring file movement that knowledge workers need every day.&lt;/p&gt;

&lt;p&gt;Syncthing is strongest when the folder remains understandable without Syncthing. If you can open the folder in a file manager and understand what it is, you are probably using the tool well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Risky Use Cases for Syncthing
&lt;/h2&gt;

&lt;p&gt;Syncthing becomes risky when users expect it to behave like backup, collaboration software, or managed cloud storage.&lt;/p&gt;

&lt;p&gt;Be careful with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using Syncthing as your only backup&lt;/li&gt;
&lt;li&gt;Syncing huge folders without thinking about deletion risk&lt;/li&gt;
&lt;li&gt;Syncing the same files while multiple apps edit them&lt;/li&gt;
&lt;li&gt;Syncing application databases that expect exclusive local access&lt;/li&gt;
&lt;li&gt;Syncing browser profiles&lt;/li&gt;
&lt;li&gt;Syncing mail stores&lt;/li&gt;
&lt;li&gt;Syncing build directories or cache folders&lt;/li&gt;
&lt;li&gt;Syncing folders with frequent generated files&lt;/li&gt;
&lt;li&gt;Syncing very large photo libraries to low-storage devices&lt;/li&gt;
&lt;li&gt;Relying on mobile background sync without testing it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The issue is not that Syncthing is unreliable. The issue is that sync is powerful. It does what you ask, including syncing deletions, conflicts, corrupted files, and accidental edits.&lt;/p&gt;

&lt;p&gt;That is why a knowledge system should combine Syncthing with versioning, snapshots, and real backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing Is Not Backup
&lt;/h2&gt;

&lt;p&gt;This point deserves its own section.&lt;/p&gt;

&lt;p&gt;Backup is about recovery. Sync is about convergence. These are related, but they are not the same.&lt;/p&gt;

&lt;p&gt;If you delete a file on one device, a sync tool may delete it everywhere. If a script corrupts a folder, the corrupted version may sync to other devices. If ransomware encrypts local files, the encrypted versions may be treated as changed files.&lt;/p&gt;

&lt;p&gt;File versioning can reduce this risk, but it does not turn Syncthing into a complete backup system. Syncthing versioning is configured per folder and per device, and it mainly protects old versions when changes are received from other devices. It does not magically preserve every local edit before it happens.&lt;/p&gt;

&lt;p&gt;A safer design is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Syncthing for active file movement.
File versioning for short-term mistake recovery.
Filesystem snapshots for local rollback.
Restic, Borg, ZFS send, Btrfs snapshots, or another backup system for real recovery.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is more boring than pretending sync is backup. It is also much safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Knowledge Workflow
&lt;/h2&gt;

&lt;p&gt;A practical self-hosted knowledge setup can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    subgraph "Desktop Workstation"
        D["Main editing device&amp;lt;br/&amp;gt;Full notes and documents"]
    end
    subgraph "Laptop"
        L["Mobile editing device&amp;lt;br/&amp;gt;Same knowledge folders"]
    end
    subgraph "Home Server or NAS"
        H["Always-on sync target&amp;lt;br/&amp;gt;Receive-side versioning&amp;lt;br/&amp;gt;Filesystem snapshots&amp;lt;br/&amp;gt;Separate backup job"]
    end
    subgraph "Phone"
        P["Selected folders only&amp;lt;br/&amp;gt;Camera scans or quick capture&amp;lt;br/&amp;gt;Avoid full archive sync"]
    end
    D &amp;lt;--&amp;gt; H
    L &amp;lt;--&amp;gt; H
    P --&amp;gt; H
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shape works because the always-on server becomes the stable point in the system. It does not need to be a central cloud server, but it gives your sync topology a reliable anchor.&lt;/p&gt;

&lt;p&gt;For knowledge management, that anchor matters. Laptops sleep. Phones throttle background services. Desktops are not always on. A small home server or NAS gives the system somewhere steady to converge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Folder Design Matters
&lt;/h2&gt;

&lt;p&gt;Do not create one giant "sync everything" folder.&lt;/p&gt;

&lt;p&gt;Create separate folders for separate purposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;knowledge-notes
knowledge-documents
research-papers
project-notes
scans-inbox
static-site-drafts
configs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you better control over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which devices receive which files&lt;/li&gt;
&lt;li&gt;Which folders need versioning&lt;/li&gt;
&lt;li&gt;Which folders need snapshots&lt;/li&gt;
&lt;li&gt;Which folders can be send-only&lt;/li&gt;
&lt;li&gt;Which folders should avoid mobile devices&lt;/li&gt;
&lt;li&gt;Which folders have privacy or size concerns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Folder boundaries are architecture. They define trust, storage, recovery, and operational behavior.&lt;/p&gt;

&lt;p&gt;For example, an Obsidian vault might sync to laptop, desktop, and server. A large PDF archive might sync only to desktop and server. A phone scan inbox might be send-only from the phone to the server.&lt;/p&gt;

&lt;p&gt;That is much cleaner than treating every device as equal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Syncthing Folder Types
&lt;/h2&gt;

&lt;p&gt;Syncthing supports different folder behavior depending on how you want changes to flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send and receive&lt;/strong&gt; is the default mode. A device both sends local changes and receives remote changes. This is the normal choice for active editing across trusted devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send-only&lt;/strong&gt; sends local changes to other devices but does not accept remote changes as authoritative. This can be useful for source folders where one device should be treated as the origin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Receive-only&lt;/strong&gt; receives changes but does not publish local changes to the rest of the cluster. This can be useful for mirrors, replication targets, and backup-adjacent destinations where local edits should not be sent back.&lt;/p&gt;

&lt;p&gt;These modes are powerful, but they can also make the system harder to reason about. Use them intentionally. For most everyday folders, send and receive is simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Good Folder Layout for Notes
&lt;/h2&gt;

&lt;p&gt;For markdown-based notes, keep the structure predictable.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;knowledge-notes/
  inbox/
  projects/
  areas/
  references/
  archive/
  attachments/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well with tools like Obsidian, VS Code, ripgrep, static site generators, and command-line scripts.&lt;/p&gt;

&lt;p&gt;Avoid putting temporary exports, generated indexes, and application caches into the same folder unless you really want them synced. Use ignore patterns for files that are noisy or machine-specific.&lt;/p&gt;

&lt;p&gt;Examples of files you may want to ignore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.DS_Store
Thumbs.db
*.tmp
*.swp
.cache/
node_modules/
dist/
build/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Obsidian specifically, think carefully before syncing all plugin state. Some settings are useful across devices, but workspace state can be annoying if it constantly changes window layouts or active panes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing and Obsidian
&lt;/h2&gt;

&lt;p&gt;Syncthing is often used to sync Obsidian vaults because Obsidian stores notes as local markdown files.&lt;/p&gt;

&lt;p&gt;This is a good pairing. Obsidian gives you the writing and linking interface. Syncthing moves the files. Your notes remain plain files.&lt;/p&gt;

&lt;p&gt;The main risks are conflicts and mobile behavior.&lt;/p&gt;

&lt;p&gt;If you edit the same note on two devices before they sync, Syncthing may create conflict files. That is better than silent data loss, but it still requires cleanup. The practical habit is to let devices sync before editing the same active note elsewhere.&lt;/p&gt;

&lt;p&gt;For mobile devices, do not assume background sync behaves exactly like desktop sync. Test it. Open the app, let sync complete, edit a note, and verify the change appears on your other devices.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://www.glukhov.org/knowledge-management/tools/obsidian-vs-logseq-comparison/" rel="noopener noreferrer"&gt;Obsidian vs Logseq comparison&lt;/a&gt; for a broader discussion of sync considerations in PKM tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing and DokuWiki or Static Knowledge Sites
&lt;/h2&gt;

&lt;p&gt;Syncthing also works well around file-based or file-friendly knowledge systems.&lt;/p&gt;

&lt;p&gt;For DokuWiki, you might use Syncthing to move exported documents, media files, or staging content between machines. Be more careful with live server data, permissions, and concurrent edits. See &lt;a href="https://www.glukhov.org/knowledge-management/self-hosted-knowledge/dokuwiki-selfhosted-wiki-alternatives/" rel="noopener noreferrer"&gt;DokuWiki and self-hosted wiki alternatives&lt;/a&gt; for platform options.&lt;/p&gt;

&lt;p&gt;For Hugo or other static site generators, Syncthing can be useful for drafts, research notes, and content source files. It should not replace Git for source control, but it can complement Git for non-code knowledge material.&lt;/p&gt;

&lt;p&gt;A good rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use Git for history and collaboration.
Use Syncthing for private file movement.
Use backup for recovery.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tool has a job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing vs Nextcloud
&lt;/h2&gt;

&lt;p&gt;Syncthing and Nextcloud are often compared, but they solve different problems.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Syncthing&lt;/th&gt;
&lt;th&gt;Nextcloud&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Peer-to-peer sync&lt;/td&gt;
&lt;td&gt;Central server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Private device-to-device folders&lt;/td&gt;
&lt;td&gt;Web-accessible file platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage model&lt;/td&gt;
&lt;td&gt;Local copies on devices&lt;/td&gt;
&lt;td&gt;Server-first storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;td&gt;Minimal local admin UI&lt;/td&gt;
&lt;td&gt;Full web file interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharing links&lt;/td&gt;
&lt;td&gt;Not the main purpose&lt;/td&gt;
&lt;td&gt;Built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Users and permissions&lt;/td&gt;
&lt;td&gt;Device trust model&lt;/td&gt;
&lt;td&gt;User and group model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendars and contacts&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Office collaboration&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Possible with add-ons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knowledge notes&lt;/td&gt;
&lt;td&gt;Good for local-first notes&lt;/td&gt;
&lt;td&gt;Good for server-centered sharing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational style&lt;/td&gt;
&lt;td&gt;Lightweight but manual&lt;/td&gt;
&lt;td&gt;Heavier but more complete&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use Syncthing when you want private file sync between your own devices.&lt;/p&gt;

&lt;p&gt;Use Nextcloud when you want a self-hosted cloud platform with users, web access, sharing, calendars, contacts, and broader collaboration features. See the &lt;a href="https://www.glukhov.org/knowledge-management/self-hosted-knowledge/nextcloud/" rel="noopener noreferrer"&gt;Nextcloud self-hosting guide&lt;/a&gt; for a detailed setup walkthrough.&lt;/p&gt;

&lt;p&gt;Use both if you have both needs. For example, Syncthing can handle your local-first notes, while Nextcloud handles family file sharing or browser-based access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing vs rsync
&lt;/h2&gt;

&lt;p&gt;Rsync is excellent for one-off or scheduled file copying. It is simple, scriptable, and widely available.&lt;/p&gt;

&lt;p&gt;Syncthing is better when you want continuous synchronization across multiple devices without writing your own scheduling, conflict detection, and device discovery logic.&lt;/p&gt;

&lt;p&gt;Use rsync for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scripted deployments&lt;/li&gt;
&lt;li&gt;One-way copies&lt;/li&gt;
&lt;li&gt;Server maintenance&lt;/li&gt;
&lt;li&gt;Simple backup jobs&lt;/li&gt;
&lt;li&gt;Predictable batch transfers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use Syncthing for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Continuous multi-device sync&lt;/li&gt;
&lt;li&gt;Local-first notes&lt;/li&gt;
&lt;li&gt;Personal document movement&lt;/li&gt;
&lt;li&gt;Always-on folder convergence&lt;/li&gt;
&lt;li&gt;Cross-platform device sync&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rsync is a tool. Syncthing is a small synchronization system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing vs Seafile
&lt;/h2&gt;

&lt;p&gt;Seafile is closer to a file sync platform. It can be a good fit when you want a central service, clients, libraries, and a more managed team file sync experience.&lt;/p&gt;

&lt;p&gt;Syncthing is more decentralized and simpler in concept. There is no main server that owns the truth. Devices share folders with each other.&lt;/p&gt;

&lt;p&gt;For personal knowledge management, Syncthing is usually easier to reason about if you already like local files. For team file sharing, Seafile or Nextcloud may be more appropriate.&lt;/p&gt;

&lt;p&gt;The question is not which tool is better. The question is whether you want local-first peer sync or a central file platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android in 2026
&lt;/h2&gt;

&lt;p&gt;The Android story needs caution.&lt;/p&gt;

&lt;p&gt;The original Syncthing Android app was discontinued after the December 2024 release. Community forks and alternative approaches may exist, but Android should not be treated as the most stable part of a Syncthing knowledge system.&lt;/p&gt;

&lt;p&gt;This does not mean Syncthing is useless on Android. It means you should design mobile sync as a convenience layer, not the only reliable copy.&lt;/p&gt;

&lt;p&gt;For Android, prefer narrower use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scan inbox&lt;/li&gt;
&lt;li&gt;Camera import folder&lt;/li&gt;
&lt;li&gt;Quick notes folder&lt;/li&gt;
&lt;li&gt;Read-only reference folder&lt;/li&gt;
&lt;li&gt;Selected documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid making your phone responsible for the only complete sync path of your knowledge archive. Phones are battery-managed, storage-limited, and increasingly restrictive about background file access.&lt;/p&gt;

&lt;p&gt;The boring recommendation is best: keep the authoritative knowledge set on desktop, laptop, and server. Let the phone participate selectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioning Strategy
&lt;/h2&gt;

&lt;p&gt;Syncthing supports file versioning, and you should usually enable it on at least one stable device.&lt;/p&gt;

&lt;p&gt;For a knowledge-management setup, the home server or NAS is often the best place for versioning. It is always on, has more storage, and is easier to include in backup jobs.&lt;/p&gt;

&lt;p&gt;Common versioning approaches include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trash can versioning&lt;/strong&gt; — moved files go to a trash folder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple versioning&lt;/strong&gt; — keeps a fixed number of old versions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staggered versioning&lt;/strong&gt; — keeps more versions near the present, fewer as files age&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External versioning&lt;/strong&gt; — delegates versioning to an external tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most personal knowledge folders, staggered versioning is a reasonable starting point. It keeps more versions near the present and fewer versions as files get older.&lt;/p&gt;

&lt;p&gt;Versioning is not free. It consumes storage and needs occasional review. But storage is cheaper than losing a year of notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snapshots and Backup
&lt;/h2&gt;

&lt;p&gt;If your Syncthing target is a NAS or Linux server, add snapshots.&lt;/p&gt;

&lt;p&gt;Good options include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ZFS snapshots&lt;/li&gt;
&lt;li&gt;Btrfs snapshots&lt;/li&gt;
&lt;li&gt;LVM snapshots&lt;/li&gt;
&lt;li&gt;restic&lt;/li&gt;
&lt;li&gt;BorgBackup&lt;/li&gt;
&lt;li&gt;Kopia&lt;/li&gt;
&lt;li&gt;Filesystem-level backup tools from your NAS platform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A strong setup might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Syncthing syncs files to the server.
The server keeps Syncthing file versions.
The filesystem keeps snapshots.
A backup tool copies encrypted backups off-device.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you multiple recovery layers.&lt;/p&gt;

&lt;p&gt;If you accidentally delete a note, Syncthing versioning may help. If a folder is corrupted, filesystem snapshots may help. If the server disk dies, off-device backup may help.&lt;/p&gt;

&lt;p&gt;That is the difference between a sync setup and a recovery strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy and Trust
&lt;/h2&gt;

&lt;p&gt;Syncthing is attractive because it lets you avoid placing all knowledge files into a third-party cloud account.&lt;/p&gt;

&lt;p&gt;However, privacy is not automatic. You still need to think about device trust.&lt;/p&gt;

&lt;p&gt;Every normal trusted device that participates in a folder can read that folder. If you sync your notes to an old laptop, that laptop is now part of your security boundary. If you sync documents to a VPS, that server matters too.&lt;/p&gt;

&lt;p&gt;Syncthing also has an untrusted encrypted device feature, but treat it carefully. It can be useful when you want an encrypted replica on a device that should not see plaintext file content, but it is more advanced and should be tested before relying on it.&lt;/p&gt;

&lt;p&gt;For most people, the simpler model is better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Only sync sensitive folders to devices you actually trust.
Encrypt disks on laptops and servers.
Back up important data separately.
Do not sync private archives everywhere.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Self-hosted does not automatically mean secure. It means you own the responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conflict Handling
&lt;/h2&gt;

&lt;p&gt;Conflicts happen when different devices change the same file before synchronization converges.&lt;/p&gt;

&lt;p&gt;For notes, this can happen if you edit the same markdown file on a laptop and desktop while one of them is offline. Syncthing will preserve conflict copies rather than silently choosing one version.&lt;/p&gt;

&lt;p&gt;That is the right behavior, but it still leaves you with cleanup work.&lt;/p&gt;

&lt;p&gt;To reduce conflicts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid editing the same note on two offline devices&lt;/li&gt;
&lt;li&gt;Let sync finish before switching machines&lt;/li&gt;
&lt;li&gt;Keep frequently edited inbox notes small&lt;/li&gt;
&lt;li&gt;Avoid syncing application state files unnecessarily&lt;/li&gt;
&lt;li&gt;Use ignore patterns for volatile files&lt;/li&gt;
&lt;li&gt;Review conflict files periodically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conflict files are not a sign that Syncthing is broken. They are a sign that two devices changed reality at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ignore Patterns
&lt;/h2&gt;

&lt;p&gt;Ignore patterns are important for knowledge folders.&lt;/p&gt;

&lt;p&gt;They keep generated, temporary, or machine-specific files out of the sync set. This reduces conflicts, saves bandwidth, and avoids polluting other devices.&lt;/p&gt;

&lt;p&gt;Common examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.DS_Store
Thumbs.db
*.tmp
*.swp
*.bak
.cache/
node_modules/
dist/
build/
__pycache__/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a notes vault, consider whether plugin caches, workspace layout files, or generated indexes should really sync. Some should. Some should not.&lt;/p&gt;

&lt;p&gt;The principle is simple: sync source material, not noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Suggested Syncthing Topologies
&lt;/h2&gt;

&lt;p&gt;For one person, a star-like topology around a home server is often easiest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;desktop &amp;lt;-&amp;gt; home-server
laptop  &amp;lt;-&amp;gt; home-server
phone   -&amp;gt; home-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The devices can still connect directly, but the server gives them a stable meeting point.&lt;/p&gt;

&lt;p&gt;For a small household, keep personal folders separate. Do not create one shared mega-folder unless everyone understands the consequences.&lt;/p&gt;

&lt;p&gt;For a small technical team, be careful. Syncthing can work for shared files among trusted peers, but it does not replace proper collaboration tools, permissions, review workflows, or version control.&lt;/p&gt;

&lt;p&gt;The more people you add, the more attractive a server-centered platform like Nextcloud, Seafile, Git, or a document system becomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Setup Example
&lt;/h2&gt;

&lt;p&gt;Here is a reasonable self-hosted knowledge setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Folders:
  knowledge-notes
  knowledge-documents
  research-papers
  scans-inbox

Devices:
  desktop
  laptop
  home-server
  phone

Rules:
  desktop and laptop use send-receive for notes
  home-server uses send-receive with versioning enabled
  phone sends scans-inbox only
  large PDFs do not sync to phone
  home-server snapshots all synced folders
  home-server backup runs nightly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup is not fancy, but it is robust.&lt;/p&gt;

&lt;p&gt;It supports local-first work, mobile capture, server-side recovery, and off-device backup. It avoids making the phone responsible for the whole archive. It also avoids confusing sync with backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational Checklist
&lt;/h2&gt;

&lt;p&gt;Before trusting Syncthing with important knowledge files, check the basics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Are all important folders synced to at least two real devices?
Is one device always on or frequently online?
Is file versioning enabled on at least one stable device?
Are filesystem snapshots enabled on the server or NAS?
Is there an off-device backup?
Are noisy files ignored?
Are mobile folders limited?
Have you tested restore?
Have you tested conflict behavior?
Do you understand which folders are send-only or receive-only?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important item is restore testing.&lt;/p&gt;

&lt;p&gt;A backup strategy you have never restored from is a theory. A sync strategy you have never tested under conflict is also a theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;The most common mistake is syncing too much.&lt;/p&gt;

&lt;p&gt;People start with one useful folder, then add every document, every photo, every export, every cache, and every application directory. The system becomes noisy and hard to reason about.&lt;/p&gt;

&lt;p&gt;The second mistake is treating receive-only folders as magical backup. They are useful, but they are not a full historical recovery system.&lt;/p&gt;

&lt;p&gt;The third mistake is ignoring deletion behavior. If deletion syncs everywhere, then deletion is part of the design.&lt;/p&gt;

&lt;p&gt;The fourth mistake is trusting mobile sync too much. Mobile operating systems are not friendly to long-running background file synchronization.&lt;/p&gt;

&lt;p&gt;The fifth mistake is not using versioning. If the data matters, keep versions somewhere.&lt;/p&gt;

&lt;p&gt;The sixth mistake is not having a real backup. Syncthing can help populate a backup target, but it should not be the only protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Syncthing Fits in a Self-Hosted Knowledge Stack
&lt;/h2&gt;

&lt;p&gt;In a self-hosted knowledge system, Syncthing is infrastructure.&lt;/p&gt;

&lt;p&gt;It sits below note-taking apps, wikis, search tools, static sites, scripts, and AI-assisted workflows. It moves the files those systems use.&lt;/p&gt;

&lt;p&gt;A simple stack might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TB
    subgraph "Capture"
        C1["Phone scanner"]
        C2["Browser save"]
        C3["Quick notes"]
    end
    subgraph "Sync"
        S["Syncthing"]
    end
    subgraph "Storage"
        ST1["Desktop"]
        ST2["Laptop"]
        ST3["Home server / NAS"]
    end
    subgraph "Authoring"
        A1["Obsidian"]
        A2["VS Code"]
        A3["Vim"]
        A4["DokuWiki"]
        A5["Static site generator"]
    end
    subgraph "Search"
        SR1["ripgrep"]
        SR2["Desktop search"]
        SR3["Local index"]
        SR4["RAG pipeline"]
    end
    subgraph "Recovery"
        R1["Versioning"]
        R2["Snapshots"]
        R3["Encrypted backup"]
    end
    C1 --&amp;gt; S
    C2 --&amp;gt; S
    C3 --&amp;gt; S
    S --&amp;gt; ST1
    S --&amp;gt; ST2
    S --&amp;gt; ST3
    ST1 --&amp;gt; A1
    ST1 --&amp;gt; A2
    ST2 --&amp;gt; A3
    ST3 --&amp;gt; A4
    ST3 --&amp;gt; A5
    A1 --&amp;gt; SR1
    A2 --&amp;gt; SR2
    A3 --&amp;gt; SR3
    A4 --&amp;gt; SR4
    ST3 --&amp;gt; R1
    ST3 --&amp;gt; R2
    ST3 --&amp;gt; R3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation is healthy.&lt;/p&gt;

&lt;p&gt;You can replace the editor without replacing the storage. You can replace the backup tool without replacing the note format. You can stop Syncthing and still have ordinary files.&lt;/p&gt;

&lt;p&gt;That is the value of local-first knowledge infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Syncthing Is the Right Tool
&lt;/h2&gt;

&lt;p&gt;Syncthing is a strong choice when the problem is private file synchronization between devices you control. It is less suitable when the real problem is collaboration, web access, permissions, or long-term recovery.&lt;/p&gt;

&lt;p&gt;This distinction matters because many self-hosted file workflows fail from unclear expectations. A sync tool can move files very well, but it should not be asked to behave like a cloud suite, a backup archive, and a team collaboration platform at the same time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Syncthing fit&lt;/th&gt;
&lt;th&gt;Better fit when Syncthing is not enough&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Private sync between your own devices&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Usually none needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local-first markdown notes&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Obsidian Sync if you prefer managed sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal document folders&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Nextcloud if you need browser access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Family file sharing&lt;/td&gt;
&lt;td&gt;Possible, but awkward&lt;/td&gt;
&lt;td&gt;Nextcloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team document collaboration&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Nextcloud, Seafile, Google Drive, Microsoft 365&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-way server copy&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;rsync may be simpler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full backup and restore&lt;/td&gt;
&lt;td&gt;Not enough alone&lt;/td&gt;
&lt;td&gt;restic, BorgBackup, Kopia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web file access&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;td&gt;Nextcloud or Seafile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendar and contacts&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Nextcloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile photo backup&lt;/td&gt;
&lt;td&gt;Possible with caution&lt;/td&gt;
&lt;td&gt;Immich, Nextcloud, platform photo tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypted offsite backup&lt;/td&gt;
&lt;td&gt;Not the main role&lt;/td&gt;
&lt;td&gt;restic, BorgBackup, Kopia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypted sync to untrusted device&lt;/td&gt;
&lt;td&gt;Advanced use case&lt;/td&gt;
&lt;td&gt;Syncthing untrusted devices, tested carefully&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the main reason Syncthing remains useful. It does not try to become a full cloud platform. It works best when the job is clear: keep selected folders synchronized across trusted devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Recommendation
&lt;/h2&gt;

&lt;p&gt;Use Syncthing for self-hosted knowledge management when you want private, local-first file sync across trusted devices.&lt;/p&gt;

&lt;p&gt;Use it for notes, documents, research folders, scans, and personal knowledge archives. Pair it with markdown, plain files, local search, and self-hosted services. Keep the structure simple enough that you can understand it without a dashboard.&lt;/p&gt;

&lt;p&gt;But do not confuse sync with backup.&lt;/p&gt;

&lt;p&gt;The best setup is not Syncthing alone. The best setup is Syncthing plus file versioning, filesystem snapshots, and real backup.&lt;/p&gt;

&lt;p&gt;That combination gives you the thing self-hosted knowledge systems should aim for: local control, practical convenience, and recoverable data.&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>backup</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Circuit Breaker Pattern in Go: Stop Cascading Failures</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sat, 18 Jul 2026 07:49:30 +0000</pubDate>
      <link>https://dev.to/rosgluk/circuit-breaker-pattern-in-go-stop-cascading-failures-7cg</link>
      <guid>https://dev.to/rosgluk/circuit-breaker-pattern-in-go-stop-cascading-failures-7cg</guid>
      <description>&lt;p&gt;A circuit breaker stops your Go service from hammering a failing dependency,&lt;br&gt;
preventing cascading failures that consume goroutines, sockets, and memory until the entire system collapses.&lt;/p&gt;



&lt;p&gt;The hard part is not the state machine. It is deciding where the breaker belongs, what counts as failure, how it interacts with timeouts and retries, and what your service should do when the circuit is open.&lt;/p&gt;

&lt;p&gt;In Go, the circuit breaker pattern is especially useful around outbound calls: HTTP APIs, payment gateways, search services, email providers, LLM gateways, internal microservices, and other dependencies that can become slow, overloaded, or partially unavailable. Used well, a circuit breaker reduces cascading failures. Used badly, it becomes another obscure failure mode.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Problem Does a Circuit Breaker Solve?
&lt;/h2&gt;

&lt;p&gt;Distributed systems rarely fail cleanly.&lt;/p&gt;

&lt;p&gt;A dependency might not be fully down. It might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;returning 500 errors&lt;/li&gt;
&lt;li&gt;returning 429 rate limit responses&lt;/li&gt;
&lt;li&gt;accepting TCP connections but never replying&lt;/li&gt;
&lt;li&gt;responding in 30 seconds instead of 300 milliseconds&lt;/li&gt;
&lt;li&gt;failing only for some requests&lt;/li&gt;
&lt;li&gt;overloaded because every client is retrying at once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The worst case is often not a hard failure. It is a slow dependency.&lt;/p&gt;

&lt;p&gt;Slow calls consume goroutines, sockets, database connections, memory, and worker capacity. If your service keeps waiting on a dependency that is already unhealthy, your service can become unhealthy too.&lt;/p&gt;

&lt;p&gt;A circuit breaker prevents that by failing fast after the dependency crosses a failure threshold.&lt;/p&gt;

&lt;p&gt;Instead of doing this forever:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request -&amp;gt; call dependency -&amp;gt; wait -&amp;gt; timeout -&amp;gt; retry -&amp;gt; wait -&amp;gt; fail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the service eventually does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request -&amp;gt; circuit open -&amp;gt; return fallback or error immediately
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fast failure is not always pleasant, but it is predictable. Predictable failure is easier to operate than a slow collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Circuit Breaker States
&lt;/h2&gt;

&lt;p&gt;Most circuit breakers use three states.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closed
&lt;/h3&gt;

&lt;p&gt;The circuit is closed during normal operation.&lt;/p&gt;

&lt;p&gt;Requests are allowed through. The breaker records successes and failures. If the number or ratio of failures crosses a threshold, the breaker opens.&lt;/p&gt;

&lt;p&gt;Closed does not mean "safe forever." It means "traffic is currently allowed."&lt;/p&gt;

&lt;h3&gt;
  
  
  Open
&lt;/h3&gt;

&lt;p&gt;The circuit is open when the dependency is considered unhealthy.&lt;/p&gt;

&lt;p&gt;Requests are rejected immediately. The service should return a fallback, cached response, degraded response, or a clear upstream error.&lt;/p&gt;

&lt;p&gt;Open does not fix the dependency. It gives the dependency time to recover and protects the caller from wasting resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Half-Open
&lt;/h3&gt;

&lt;p&gt;After a cool-down period, the breaker enters a half-open state.&lt;/p&gt;

&lt;p&gt;Only a limited number of trial requests are allowed through. If they succeed, the breaker closes. If they fail, the breaker opens again.&lt;/p&gt;

&lt;p&gt;Half-open is important because it avoids two bad extremes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;never trying the dependency again&lt;/li&gt;
&lt;li&gt;sending full traffic back too quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The state transitions look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; Closed
    Closed --&amp;gt; Open: Failure threshold reached
    Open --&amp;gt; HalfOpen: Timeout elapsed
    HalfOpen --&amp;gt; Closed: Trial succeeds
    HalfOpen --&amp;gt; Open: Trial fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Circuit Breaker vs Timeout vs Retry
&lt;/h2&gt;

&lt;p&gt;A common mistake is treating circuit breakers, retries, and timeouts as interchangeable. They are related, but they solve different problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeout
&lt;/h3&gt;

&lt;p&gt;A timeout limits how long one operation can run.&lt;/p&gt;

&lt;p&gt;In Go, this usually means passing a &lt;code&gt;context.Context&lt;/code&gt; with a deadline or timeout into the outbound call.&lt;/p&gt;

&lt;p&gt;A timeout answers this question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How long am I willing to wait for this one call?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Retry
&lt;/h3&gt;

&lt;p&gt;A retry repeats an operation when the failure might be temporary.&lt;/p&gt;

&lt;p&gt;Retries are useful for short network glitches, temporary 503 responses, connection resets, and other transient failures.&lt;/p&gt;

&lt;p&gt;A retry answers this question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Should I try this call again?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Circuit Breaker
&lt;/h3&gt;

&lt;p&gt;A circuit breaker stops calls when the dependency is probably unhealthy.&lt;/p&gt;

&lt;p&gt;It answers this question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Should I call this dependency at all right now?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rate Limiter
&lt;/h3&gt;

&lt;p&gt;A rate limiter controls how much traffic is allowed over time.&lt;/p&gt;

&lt;p&gt;It answers this question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How much traffic should this caller send?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bulkhead
&lt;/h3&gt;

&lt;p&gt;A bulkhead isolates resources so one dependency cannot consume everything.&lt;/p&gt;

&lt;p&gt;It answers this question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How much of my service can this dependency damage?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These patterns are strongest when used together. A circuit breaker without timeouts is weak. Retries without jitter can create retry storms. A fallback without metrics can hide an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use a Circuit Breaker in Go
&lt;/h2&gt;

&lt;p&gt;Use a circuit breaker when your service calls a dependency that can fail independently from your service.&lt;/p&gt;

&lt;p&gt;Good candidates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;external HTTP APIs&lt;/li&gt;
&lt;li&gt;payment processors&lt;/li&gt;
&lt;li&gt;email and SMS providers&lt;/li&gt;
&lt;li&gt;search services&lt;/li&gt;
&lt;li&gt;recommendation services&lt;/li&gt;
&lt;li&gt;LLM inference gateways&lt;/li&gt;
&lt;li&gt;internal microservice endpoints&lt;/li&gt;
&lt;li&gt;third-party SaaS APIs&lt;/li&gt;
&lt;li&gt;slow or overloaded read-side services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Circuit breakers are especially useful when the caller can degrade gracefully.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;return cached product data&lt;/li&gt;
&lt;li&gt;skip a recommendation block&lt;/li&gt;
&lt;li&gt;mark a payment provider as temporarily unavailable&lt;/li&gt;
&lt;li&gt;queue work for later&lt;/li&gt;
&lt;li&gt;return a partial response&lt;/li&gt;
&lt;li&gt;fail fast with a clear temporary error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important question is not "can this call fail?" Everything can fail. The better question is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If this dependency is failing, should we continue sending full traffic to it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the answer is no, a circuit breaker may help.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Use a Circuit Breaker
&lt;/h2&gt;

&lt;p&gt;Do not add a circuit breaker to every function just because the pattern sounds responsible.&lt;/p&gt;

&lt;p&gt;A circuit breaker is usually not useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local in-process function calls&lt;/li&gt;
&lt;li&gt;simple CRUD inside a monolith&lt;/li&gt;
&lt;li&gt;validation logic&lt;/li&gt;
&lt;li&gt;deterministic business rules&lt;/li&gt;
&lt;li&gt;CPU-only local operations&lt;/li&gt;
&lt;li&gt;code paths where no useful fallback exists&lt;/li&gt;
&lt;li&gt;write operations that are not idempotent&lt;/li&gt;
&lt;li&gt;dependencies already protected by a stronger workflow layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A circuit breaker also does not replace basic hygiene:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set timeouts&lt;/li&gt;
&lt;li&gt;propagate context&lt;/li&gt;
&lt;li&gt;use connection pools correctly&lt;/li&gt;
&lt;li&gt;handle errors explicitly&lt;/li&gt;
&lt;li&gt;make retries safe&lt;/li&gt;
&lt;li&gt;observe failure rates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A bad circuit breaker can make a system harder to reason about. It can hide the real problem, reject traffic too aggressively, or create confusing behavior during recovery.&lt;/p&gt;

&lt;p&gt;The slightly opinionated rule is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add circuit breakers at dependency boundaries, not everywhere.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Choosing a Go Circuit Breaker Library
&lt;/h2&gt;

&lt;p&gt;You can implement a basic circuit breaker yourself, but most production Go services should use a library.&lt;/p&gt;

&lt;p&gt;The most common simple choice is &lt;code&gt;sony/gobreaker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;closed, open, and half-open states&lt;/li&gt;
&lt;li&gt;configurable failure thresholds&lt;/li&gt;
&lt;li&gt;configurable open-state timeout&lt;/li&gt;
&lt;li&gt;state change callbacks&lt;/li&gt;
&lt;li&gt;request counters&lt;/li&gt;
&lt;li&gt;generic support in v2&lt;/li&gt;
&lt;li&gt;a small API surface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For larger resilience pipelines, you may also look at libraries that compose multiple policies, such as retry, timeout, fallback, rate limiting, bulkhead isolation, and circuit breaking. That can be useful when you want a single resilience layer around an operation.&lt;/p&gt;

&lt;p&gt;For many Go services, though, &lt;code&gt;gobreaker&lt;/code&gt; is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go Circuit Breaker Packages Compared
&lt;/h2&gt;

&lt;p&gt;Go does not include a built-in circuit breaker in the standard library. In practice, you usually choose between a small circuit breaker library, a larger resilience framework, or an older Hystrix-style package.&lt;/p&gt;

&lt;p&gt;For most new Go services, the decision is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;code&gt;sony/gobreaker&lt;/code&gt; if you want a small, focused circuit breaker&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;failsafe-go&lt;/code&gt; if you want circuit breakers composed with retries, timeouts, fallbacks, bulkheads, rate limits, and other resilience policies&lt;/li&gt;
&lt;li&gt;avoid starting new projects on &lt;code&gt;hystrix-go&lt;/code&gt; unless you already have legacy code using it&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Strengths&lt;/th&gt;
&lt;th&gt;Tradeoffs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sony/gobreaker/v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Simple circuit breakers around HTTP/RPC clients&lt;/td&gt;
&lt;td&gt;Small API, generic v2 support, clear state model, easy to wrap dependency clients&lt;/td&gt;
&lt;td&gt;Only solves circuit breaking; retries, timeouts, and fallbacks must be composed separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failsafe-go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full resilience policy composition&lt;/td&gt;
&lt;td&gt;Retry, fallback, circuit breaker, timeout, bulkhead, rate limiter, cache, hedge, adaptive limiter, and adaptive throttler policies&lt;/td&gt;
&lt;td&gt;More concepts to learn; heavier than needed if you only want a basic breaker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;afex/hystrix-go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Legacy Hystrix-style systems&lt;/td&gt;
&lt;td&gt;Familiar Hystrix concepts, command-style execution, historical usage&lt;/td&gt;
&lt;td&gt;Older design; not the best default for new Go services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;go-kit/kit/circuitbreaker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go kit endpoint-based services&lt;/td&gt;
&lt;td&gt;Fits Go kit middleware style and endpoint architecture&lt;/td&gt;
&lt;td&gt;Mostly useful if your service already uses Go kit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cep21/circuit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hystrix-like circuit breaker behavior&lt;/td&gt;
&lt;td&gt;More featureful Hystrix-style approach&lt;/td&gt;
&lt;td&gt;Less common as the simple default; may be more than needed for small services&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My default recommendation is boring on purpose: start with &lt;code&gt;sony/gobreaker/v2&lt;/code&gt; when you only need a circuit breaker. Reach for &lt;code&gt;failsafe-go&lt;/code&gt; when you want to express a complete resilience policy in one place.&lt;/p&gt;

&lt;p&gt;That split keeps the architecture clean. A small service client does not need a full resilience framework just to stop calling a failing dependency. But a gateway, aggregator, API client SDK, or high-traffic integration layer may benefit from composed policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing gobreaker
&lt;/h2&gt;

&lt;p&gt;Use the v2 package for new code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/sony/gobreaker/v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then import it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/sony/gobreaker/v2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Basic Circuit Breaker in Go
&lt;/h2&gt;

&lt;p&gt;Here is a small example around an HTTP call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/sony/gobreaker/v2"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ErrTemporaryUnavailable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dependency temporarily unavailable"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserClient&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;baseURL&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;
    &lt;span class="n"&gt;cb&lt;/span&gt;      &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;[[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewUserClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseURL&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;"user-service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;MaxRequests&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Interval&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ReadyToTrip&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConsecutiveFailures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;OnStateChange&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"circuit breaker %s changed from %s to %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;UserClient&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;[[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodGet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;"/users/"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user service returned %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user not found"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user service client error: %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrOpenState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrTemporaryUnavailable&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrTooManyRequests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrTemporaryUnavailable&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a complete production client, but it shows the shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the breaker wraps the outbound call&lt;/li&gt;
&lt;li&gt;the HTTP request receives a context&lt;/li&gt;
&lt;li&gt;the HTTP client has a timeout&lt;/li&gt;
&lt;li&gt;server-side failures count as breaker failures&lt;/li&gt;
&lt;li&gt;open circuit errors are mapped into an application error&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configuring gobreaker Settings
&lt;/h2&gt;

&lt;p&gt;The key settings are worth understanding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Name
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Name&lt;/code&gt; identifies the breaker.&lt;/p&gt;

&lt;p&gt;Use a stable, specific name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment-api
search-service
llm-gateway
user-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid vague names like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http-client
external-call
default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will want this name in logs and metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  MaxRequests
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;MaxRequests&lt;/code&gt; controls how many requests are allowed while the breaker is half-open.&lt;/p&gt;

&lt;p&gt;A small number is usually safer. The purpose of half-open is to test recovery, not to send full traffic immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interval
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Interval&lt;/code&gt; controls when internal counts are cleared while the breaker is closed.&lt;/p&gt;

&lt;p&gt;If it is zero, counts are not cleared automatically. A non-zero interval gives the breaker a rolling-ish memory window, although it is not the same as a full sliding window implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeout
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Timeout&lt;/code&gt; controls how long the breaker stays open before moving to half-open.&lt;/p&gt;

&lt;p&gt;If the timeout is too short, your service will keep probing a dependency that has not recovered. If it is too long, recovery will be delayed.&lt;/p&gt;

&lt;p&gt;Start with something conservative, such as 10 to 30 seconds, then tune from production metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  ReadyToTrip
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ReadyToTrip&lt;/code&gt; decides when the breaker should open.&lt;/p&gt;

&lt;p&gt;A simple rule is consecutive failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ReadyToTrip&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConsecutiveFailures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is easy to reason about, but it may not be right for high-volume services.&lt;/p&gt;

&lt;p&gt;Another option is failure ratio after a minimum number of requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ReadyToTrip&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="n"&gt;gobreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Requests&lt;/span&gt;
    &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalFailures&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids opening the circuit after a tiny sample size.&lt;/p&gt;

&lt;h3&gt;
  
  
  OnStateChange
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;OnStateChange&lt;/code&gt; is where you should emit logs or metrics.&lt;/p&gt;

&lt;p&gt;At minimum, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;breaker name&lt;/li&gt;
&lt;li&gt;old state&lt;/li&gt;
&lt;li&gt;new state&lt;/li&gt;
&lt;li&gt;timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For production systems, expose breaker state as a metric. Logs are useful for debugging, but metrics are better for alerting and dashboards.&lt;/p&gt;

&lt;h3&gt;
  
  
  IsSuccessful
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;IsSuccessful&lt;/code&gt; lets you decide which errors count as failures.&lt;/p&gt;

&lt;p&gt;This is important.&lt;/p&gt;

&lt;p&gt;Not every error should open the breaker. For example, a &lt;code&gt;404 Not Found&lt;/code&gt; from a user service may be a valid business result. A &lt;code&gt;400 Bad Request&lt;/code&gt; might be the caller's fault, not the dependency's fault.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;503 Service Unavailable&lt;/code&gt;, timeout, connection reset, or &lt;code&gt;429 Too Many Requests&lt;/code&gt; may be a real dependency health signal.&lt;/p&gt;

&lt;p&gt;Be careful here. Counting the wrong errors is one of the easiest ways to build a noisy circuit breaker.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Count as Failure?
&lt;/h2&gt;

&lt;p&gt;This is where engineering judgement matters.&lt;/p&gt;

&lt;p&gt;Usually count these as failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;network timeouts&lt;/li&gt;
&lt;li&gt;connection refused&lt;/li&gt;
&lt;li&gt;connection reset&lt;/li&gt;
&lt;li&gt;HTTP 500&lt;/li&gt;
&lt;li&gt;HTTP 502&lt;/li&gt;
&lt;li&gt;HTTP 503&lt;/li&gt;
&lt;li&gt;HTTP 504&lt;/li&gt;
&lt;li&gt;repeated 429 responses&lt;/li&gt;
&lt;li&gt;malformed responses from the dependency&lt;/li&gt;
&lt;li&gt;context deadline exceeded during the outbound call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Usually do not count these as dependency failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validation errors&lt;/li&gt;
&lt;li&gt;local serialization errors&lt;/li&gt;
&lt;li&gt;expected 404 responses&lt;/li&gt;
&lt;li&gt;caller-side authorization failures&lt;/li&gt;
&lt;li&gt;business rule rejections&lt;/li&gt;
&lt;li&gt;user input errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The breaker should represent dependency health, not general application failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breakers and context.Context
&lt;/h2&gt;

&lt;p&gt;In Go, circuit breakers should not replace &lt;code&gt;context.Context&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A circuit breaker decides whether to attempt a call. A context controls how long that call may run and whether it should stop when the caller is gone.&lt;/p&gt;

&lt;p&gt;A good outbound call should usually have both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parentCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context should flow through the call chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;incoming request context
-&amp;gt; service method
-&amp;gt; client method
-&amp;gt; HTTP request
-&amp;gt; dependency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid creating detached background contexts inside request-scoped code. If the user request is canceled, the downstream work should usually stop too.&lt;/p&gt;

&lt;p&gt;The calm rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The breaker protects the system. The context protects the request.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You normally need both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breakers and Retries
&lt;/h2&gt;

&lt;p&gt;Retries and circuit breakers can work well together, but the order matters.&lt;/p&gt;

&lt;p&gt;The safest default is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timeout per attempt
retry with backoff and jitter
circuit breaker around the dependency call
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But there is no universal answer. Think about what you want to count.&lt;/p&gt;

&lt;p&gt;If each retry attempt passes through the breaker, one user request can contribute multiple failures. That may open the breaker faster, which can be good or bad.&lt;/p&gt;

&lt;p&gt;If the breaker wraps the whole retry operation, the breaker sees one final success or failure per user request. That is calmer, but it may hide the number of failed attempts.&lt;/p&gt;

&lt;p&gt;For many application services, this shape is reasonable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user request
-&amp;gt; circuit breaker
   -&amp;gt; retry policy
      -&amp;gt; one HTTP attempt with timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the breaker tracks whether the dependency operation ultimately worked for the caller.&lt;/p&gt;

&lt;p&gt;For lower-level clients, this shape can also make sense:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user request
-&amp;gt; retry policy
   -&amp;gt; circuit breaker
      -&amp;gt; one HTTP attempt with timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the breaker protects each attempt.&lt;/p&gt;

&lt;p&gt;The more important rule is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not retry blindly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;a small maximum retry count&lt;/li&gt;
&lt;li&gt;exponential backoff&lt;/li&gt;
&lt;li&gt;jitter&lt;/li&gt;
&lt;li&gt;per-attempt timeouts&lt;/li&gt;
&lt;li&gt;an overall request deadline&lt;/li&gt;
&lt;li&gt;idempotency for writes&lt;/li&gt;
&lt;li&gt;metrics for retry attempts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those, retries can turn a small outage into a larger one. For a deeper treatment of retry safety, see &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/idempotency-in-distributed-systems/" rel="noopener noreferrer"&gt;Idempotency in Distributed Systems That Actually Works&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breakers and Idempotency
&lt;/h2&gt;

&lt;p&gt;Circuit breakers often appear next to retries, and retries raise the question of idempotency.&lt;/p&gt;

&lt;p&gt;For read operations, retrying is usually safe.&lt;/p&gt;

&lt;p&gt;For write operations, retrying can be dangerous.&lt;/p&gt;

&lt;p&gt;Consider this payment call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /charge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the request times out, did the payment fail? Maybe. Did it succeed but the response was lost? Also maybe.&lt;/p&gt;

&lt;p&gt;If you retry without an idempotency key, you might charge twice.&lt;/p&gt;

&lt;p&gt;For write operations, use one or more of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;idempotency keys&lt;/li&gt;
&lt;li&gt;request IDs&lt;/li&gt;
&lt;li&gt;operation IDs&lt;/li&gt;
&lt;li&gt;unique constraints&lt;/li&gt;
&lt;li&gt;transactional outbox&lt;/li&gt;
&lt;li&gt;workflow orchestration&lt;/li&gt;
&lt;li&gt;explicit reconciliation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A circuit breaker can stop you from continuing to call a failing payment provider, but it cannot make unsafe retries safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breakers and Fallbacks
&lt;/h2&gt;

&lt;p&gt;When the circuit is open, your service needs a plan.&lt;/p&gt;

&lt;p&gt;Possible fallback strategies include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;return cached data&lt;/li&gt;
&lt;li&gt;return stale data with a warning&lt;/li&gt;
&lt;li&gt;omit a non-critical section&lt;/li&gt;
&lt;li&gt;queue work for later&lt;/li&gt;
&lt;li&gt;switch to another provider&lt;/li&gt;
&lt;li&gt;return a temporary error&lt;/li&gt;
&lt;li&gt;show degraded functionality&lt;/li&gt;
&lt;li&gt;fail the request quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fallback should be honest.&lt;/p&gt;

&lt;p&gt;For example, this is usually good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"temporary_unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Recommendations are temporarily unavailable"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is risky:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty list may look like a valid result. It can hide an outage, confuse users, and make debugging harder.&lt;/p&gt;

&lt;p&gt;Silent fallbacks are tempting. They are also dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breakers and Observability
&lt;/h2&gt;

&lt;p&gt;A circuit breaker without observability is mostly a surprise generator.&lt;/p&gt;

&lt;p&gt;Track at least these metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;current breaker state&lt;/li&gt;
&lt;li&gt;state changes&lt;/li&gt;
&lt;li&gt;calls allowed&lt;/li&gt;
&lt;li&gt;calls rejected&lt;/li&gt;
&lt;li&gt;successes&lt;/li&gt;
&lt;li&gt;failures&lt;/li&gt;
&lt;li&gt;timeouts&lt;/li&gt;
&lt;li&gt;fallback responses&lt;/li&gt;
&lt;li&gt;retry attempts&lt;/li&gt;
&lt;li&gt;downstream latency&lt;/li&gt;
&lt;li&gt;downstream status codes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful labels include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;breaker name&lt;/li&gt;
&lt;li&gt;dependency name&lt;/li&gt;
&lt;li&gt;operation name&lt;/li&gt;
&lt;li&gt;status class&lt;/li&gt;
&lt;li&gt;error category&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid high-cardinality labels such as user ID, full URL, request ID, or raw error messages.&lt;/p&gt;

&lt;p&gt;You should be able to answer these questions from dashboards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which circuit breakers are open right now?&lt;/li&gt;
&lt;li&gt;How often do they open?&lt;/li&gt;
&lt;li&gt;Which dependency caused the opening?&lt;/li&gt;
&lt;li&gt;Are users seeing fallback responses?&lt;/li&gt;
&lt;li&gt;Did latency improve after the breaker opened?&lt;/li&gt;
&lt;li&gt;Did retry volume spike before the breaker opened?&lt;/li&gt;
&lt;li&gt;Did the dependency recover?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot observe the breaker, you cannot tune it. For structured logging that pairs well with metrics, see &lt;a href="https://www.glukhov.org/observability/logging/structured-logging-go-slog/" rel="noopener noreferrer"&gt;Structured Logging in Go with slog&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A More Production-Friendly HTTP Client Shape
&lt;/h2&gt;

&lt;p&gt;For real services, avoid scattering circuit breaker logic across handlers.&lt;/p&gt;

&lt;p&gt;Create a small client package around the dependency.&lt;/p&gt;

&lt;p&gt;Example structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal/
  userservice/
    client.go
    errors.go
    metrics.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler should not know the details of gobreaker. It should depend on a domain-level client method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserService&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the implementation can contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP request creation&lt;/li&gt;
&lt;li&gt;context propagation&lt;/li&gt;
&lt;li&gt;breaker execution&lt;/li&gt;
&lt;li&gt;status code handling&lt;/li&gt;
&lt;li&gt;response decoding&lt;/li&gt;
&lt;li&gt;metrics&lt;/li&gt;
&lt;li&gt;error mapping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the resilience policy close to the dependency boundary. For more on error classification at boundaries, see &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-error-handling-architecture/" rel="noopener noreferrer"&gt;Go Error Handling Architecture: Boundaries and Patterns&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Circuit Breakers Fit in Application Architecture
&lt;/h2&gt;

&lt;p&gt;The circuit breaker pattern belongs at integration boundaries.&lt;/p&gt;

&lt;p&gt;In a Go application, that usually means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph LR
    A[Handler] --&amp;gt; B[Application Service]
    B --&amp;gt; C[Dependency Client]
    C --&amp;gt; D[Circuit Breaker]
    D --&amp;gt; E[HTTP / RPC / DB / Queue]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the breaker out of business logic when possible.&lt;/p&gt;

&lt;p&gt;The business layer should understand domain errors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment provider unavailable
recommendations unavailable
profile service timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should not need to understand gobreaker states.&lt;/p&gt;

&lt;p&gt;This separation keeps the architecture clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transport concerns stay in clients&lt;/li&gt;
&lt;li&gt;resilience policy stays near dependencies&lt;/li&gt;
&lt;li&gt;domain logic stays readable&lt;/li&gt;
&lt;li&gt;handlers stay thin&lt;/li&gt;
&lt;li&gt;tests are easier to write&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: No Timeout
&lt;/h3&gt;

&lt;p&gt;A circuit breaker does not magically stop slow calls unless the calls return.&lt;/p&gt;

&lt;p&gt;If the outbound operation can hang forever, the breaker may not see a failure quickly enough.&lt;/p&gt;

&lt;p&gt;Always use timeouts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: One Global Breaker for Everything
&lt;/h3&gt;

&lt;p&gt;Do not use one breaker for all dependencies.&lt;/p&gt;

&lt;p&gt;A failing email provider should not open the circuit for your payment provider. A slow search endpoint should not block user profile calls.&lt;/p&gt;

&lt;p&gt;Use separate breakers for separate dependency operations when their failure modes differ.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Counting Caller Errors as Dependency Failures
&lt;/h3&gt;

&lt;p&gt;If your service sends bad input and receives &lt;code&gt;400 Bad Request&lt;/code&gt;, that is usually not a downstream outage.&lt;/p&gt;

&lt;p&gt;Do not train the breaker on your own bugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: Retrying Non-Idempotent Writes
&lt;/h3&gt;

&lt;p&gt;Retries are not free. They can duplicate writes, payments, messages, or side effects.&lt;/p&gt;

&lt;p&gt;Make writes idempotent before retrying them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 5: Hiding Outages Behind Fallbacks
&lt;/h3&gt;

&lt;p&gt;Fallbacks should degrade gracefully, not falsify reality.&lt;/p&gt;

&lt;p&gt;If a dependency is down, your metrics and logs should make that obvious.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 6: Tuning Without Production Data
&lt;/h3&gt;

&lt;p&gt;Thresholds copied from examples are only starting points.&lt;/p&gt;

&lt;p&gt;Tune based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request volume&lt;/li&gt;
&lt;li&gt;normal error rate&lt;/li&gt;
&lt;li&gt;dependency latency&lt;/li&gt;
&lt;li&gt;user impact&lt;/li&gt;
&lt;li&gt;recovery time&lt;/li&gt;
&lt;li&gt;fallback quality&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mistake 7: Using Circuit Breakers Instead of Capacity Management
&lt;/h3&gt;

&lt;p&gt;A circuit breaker is not a substitute for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;load shedding&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;li&gt;queue limits&lt;/li&gt;
&lt;li&gt;autoscaling&lt;/li&gt;
&lt;li&gt;database tuning&lt;/li&gt;
&lt;li&gt;connection pool limits&lt;/li&gt;
&lt;li&gt;upstream quotas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is one part of a resilience strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Defaults
&lt;/h2&gt;

&lt;p&gt;For a typical Go service calling an internal HTTP dependency, a reasonable starting point might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP client timeout: 2 to 5 seconds
per-request context timeout: based on caller SLA
breaker failure rule: 5 consecutive failures or 50 percent failure after 20 requests
open timeout: 10 to 30 seconds
half-open requests: 1 to 5
retry count: 1 to 3 attempts
retry backoff: exponential with jitter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are not universal values. They are safe-ish starting points.&lt;/p&gt;

&lt;p&gt;For user-facing APIs, keep total latency budgets tight. For background jobs, you may tolerate longer waits. For payment providers, be much more careful with retries and idempotency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit Breaker Checklist
&lt;/h2&gt;

&lt;p&gt;Before adding a circuit breaker, answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What dependency is being protected?&lt;/li&gt;
&lt;li&gt;What operation is being protected?&lt;/li&gt;
&lt;li&gt;What errors count as dependency failure?&lt;/li&gt;
&lt;li&gt;What errors should be ignored by the breaker?&lt;/li&gt;
&lt;li&gt;What timeout applies to each call?&lt;/li&gt;
&lt;li&gt;Are retries allowed?&lt;/li&gt;
&lt;li&gt;Are writes idempotent?&lt;/li&gt;
&lt;li&gt;What happens when the circuit is open?&lt;/li&gt;
&lt;li&gt;Is there a fallback?&lt;/li&gt;
&lt;li&gt;Is the fallback visible in metrics?&lt;/li&gt;
&lt;li&gt;Who gets alerted if the circuit keeps opening?&lt;/li&gt;
&lt;li&gt;How will the breaker be tuned after deployment?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot answer these, adding a breaker may create more confusion than resilience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Circuit Breakers in Go
&lt;/h2&gt;

&lt;p&gt;Test behavior, not the internal state machine of the library.&lt;/p&gt;

&lt;p&gt;Useful tests include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dependency succeeds and response is returned&lt;/li&gt;
&lt;li&gt;dependency fails repeatedly and circuit opens&lt;/li&gt;
&lt;li&gt;open circuit returns a temporary error&lt;/li&gt;
&lt;li&gt;client-side validation errors do not trip the breaker&lt;/li&gt;
&lt;li&gt;context timeout is respected&lt;/li&gt;
&lt;li&gt;fallback response is returned when expected&lt;/li&gt;
&lt;li&gt;metrics are emitted on state changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use fake HTTP servers for integration-style tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httptest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusServiceUnavailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For unit tests, hide the dependency behind an interface and inject a fake implementation.&lt;/p&gt;

&lt;p&gt;Keep tests deterministic. Avoid sleeping for long real durations. Configure short breaker timeouts in tests. For more on testing concurrent Go code with fake time and isolated bubbles, see &lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/testing-concurrent-go-code-synctest/" rel="noopener noreferrer"&gt;Testing Concurrent Go Code with testing/synctest&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Build Your Own Circuit Breaker?
&lt;/h2&gt;

&lt;p&gt;Building a small circuit breaker is a good learning exercise. It helps you understand the state machine.&lt;/p&gt;

&lt;p&gt;For production code, prefer a maintained library unless your needs are very specific.&lt;/p&gt;

&lt;p&gt;A production breaker needs to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;concurrency&lt;/li&gt;
&lt;li&gt;state transitions&lt;/li&gt;
&lt;li&gt;counters&lt;/li&gt;
&lt;li&gt;half-open probes&lt;/li&gt;
&lt;li&gt;callbacks&lt;/li&gt;
&lt;li&gt;custom failure classification&lt;/li&gt;
&lt;li&gt;race-free behavior&lt;/li&gt;
&lt;li&gt;predictable error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not impossible, but it is easy to get subtly wrong.&lt;/p&gt;

&lt;p&gt;The boring library is usually the better choice.&lt;/p&gt;

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

&lt;p&gt;The circuit breaker pattern is not magic reliability dust.&lt;/p&gt;

&lt;p&gt;In Go, it works best when it is part of a small, explicit resilience stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;context timeout
+ retry with backoff and jitter
+ circuit breaker
+ fallback
+ metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is most useful at dependency boundaries, especially around remote services that can become slow or partially unavailable.&lt;/p&gt;

&lt;p&gt;Use it to stop cascading failures. Use it to fail fast when a dependency is clearly unhealthy. Use it to give overloaded systems room to recover.&lt;/p&gt;

&lt;p&gt;But do not use it as an excuse to ignore timeouts, idempotency, observability, or clean architecture.&lt;/p&gt;

&lt;p&gt;A good circuit breaker makes failure clearer and cheaper. A bad one just makes failure more mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/go-microservices-for-ai-ml-orchestration-patterns/" rel="noopener noreferrer"&gt;Go Microservices for AI/ML Orchestration&lt;/a&gt; — broader orchestration context where circuit breakers fit&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/saga-pattern-distributed-transactions/" rel="noopener noreferrer"&gt;Saga Pattern in Distributed Transactions&lt;/a&gt; — distributed transaction patterns that pair with circuit breakers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/idempotency-in-distributed-systems/" rel="noopener noreferrer"&gt;Idempotency in Distributed Systems&lt;/a&gt; — retry safety and idempotent operations&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/transactional-outbox-pattern-go/" rel="noopener noreferrer"&gt;Transactional Outbox Pattern in Go&lt;/a&gt; — reliable event delivery alongside resilience patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-error-handling-architecture/" rel="noopener noreferrer"&gt;Go Error Handling Architecture&lt;/a&gt; — error classification at dependency boundaries&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/testing-concurrent-go-code-synctest/" rel="noopener noreferrer"&gt;Testing Concurrent Go Code with synctest&lt;/a&gt; — testing async behavior with circuit breakers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/observability/logging/structured-logging-go-slog/" rel="noopener noreferrer"&gt;Structured Logging in Go with slog&lt;/a&gt; — observability alongside circuit breakers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;github.com/sony/gobreaker/v2&lt;/code&gt; — official gobreaker v2 package&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-context-cancellation-timeouts/" rel="noopener noreferrer"&gt;Go Context Cancellation and Timeouts&lt;/a&gt; — context patterns that pair with circuit breakers&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>architecture</category>
      <category>dev</category>
    </item>
    <item>
      <title>Podman Quadlet vs Docker Compose for Linux Services</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Thu, 16 Jul 2026 10:13:30 +0000</pubDate>
      <link>https://dev.to/rosgluk/podman-quadlet-vs-docker-compose-for-linux-services-gci</link>
      <guid>https://dev.to/rosgluk/podman-quadlet-vs-docker-compose-for-linux-services-gci</guid>
      <description>&lt;p&gt;Docker Compose and Podman Quadlet solve overlapping problems but come from different design centers, and choosing between them depends on whether you think in application stacks or Linux services.&lt;/p&gt;

&lt;p&gt;The distinction matters for anyone running containers on a Linux host beyond a single afternoon of experimentation. Compose describes services, networks, and volumes in a YAML file and starts them with &lt;code&gt;docker compose up&lt;/code&gt;. Quadlet describes containers in systemd-style unit files and lets the system service manager own the lifecycle.&lt;/p&gt;

&lt;p&gt;Both approaches work for self-hosted services, internal tools, and small servers. The right choice comes down to your operational model, team familiarity, and whether you prefer a developer-friendly stack format or a systemd-native service model. This comparison covers the practical differences: file formats, lifecycle ownership, rootless containers, logging, updates, networking, security, and migration paths. It is part of &lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools: The Complete Guide to Modern Development Workflows&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Recommendation
&lt;/h2&gt;

&lt;p&gt;Use Docker Compose when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want the fastest multi-container workflow.&lt;/li&gt;
&lt;li&gt;You already use &lt;a href="https://www.glukhov.org/developer-tools/containers/install-docker-on-ubuntu/" rel="noopener noreferrer"&gt;Docker Engine&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;You share stacks with developers.&lt;/li&gt;
&lt;li&gt;You need a familiar &lt;code&gt;compose.yaml&lt;/code&gt; for local development.&lt;/li&gt;
&lt;li&gt;You deploy small services with Docker on one host.&lt;/li&gt;
&lt;li&gt;You use existing Compose examples from projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use Podman Quadlet when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want systemd-native container services.&lt;/li&gt;
&lt;li&gt;You prefer rootless containers.&lt;/li&gt;
&lt;li&gt;You do not want a central Docker daemon.&lt;/li&gt;
&lt;li&gt;You run long-lived services on a Linux host.&lt;/li&gt;
&lt;li&gt;You want &lt;code&gt;systemctl&lt;/code&gt;, &lt;code&gt;journalctl&lt;/code&gt;, timers, dependencies, and auto-start.&lt;/li&gt;
&lt;li&gt;You are building a self-hosted or homelab server around systemd.&lt;/li&gt;
&lt;li&gt;You want containers to fit into the normal Linux service model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker Compose is better for application stacks.
Podman Quadlet is better for Linux services.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a law. It is a useful default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Docker Compose&lt;/th&gt;
&lt;th&gt;Podman Quadlet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary model&lt;/td&gt;
&lt;td&gt;Multi-container application&lt;/td&gt;
&lt;td&gt;systemd-managed container service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File format&lt;/td&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;td&gt;systemd-like unit files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Docker Engine&lt;/td&gt;
&lt;td&gt;Podman&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daemon&lt;/td&gt;
&lt;td&gt;Uses Docker daemon&lt;/td&gt;
&lt;td&gt;Daemonless Podman model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service manager&lt;/td&gt;
&lt;td&gt;Compose manages stack lifecycle&lt;/td&gt;
&lt;td&gt;systemd manages lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Dev stacks, app bundles, simple deployments&lt;/td&gt;
&lt;td&gt;Long-running Linux services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rootless support&lt;/td&gt;
&lt;td&gt;Possible, but not the default mental model&lt;/td&gt;
&lt;td&gt;Strong fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;journalctl&lt;/code&gt; and &lt;code&gt;podman logs&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup on boot&lt;/td&gt;
&lt;td&gt;Usually via systemd wrapper or restart policy&lt;/td&gt;
&lt;td&gt;Native systemd unit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updates&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Podman auto-update or systemd workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portability&lt;/td&gt;
&lt;td&gt;Very high across Docker environments&lt;/td&gt;
&lt;td&gt;Best on Linux with systemd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Easier for most developers&lt;/td&gt;
&lt;td&gt;Easier for systemd users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem examples&lt;/td&gt;
&lt;td&gt;Huge&lt;/td&gt;
&lt;td&gt;Smaller, but growing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither one is Kubernetes. Most small services do not need a cluster. They need a boring, understandable way to start, stop, update, log, and recover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Docker Compose Is Good At
&lt;/h2&gt;

&lt;p&gt;Docker Compose is a tool for defining and running multi-container applications. A typical Compose file describes services, images, build contexts, ports, volumes, networks, environment variables, health checks, dependencies, and profiles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:stable&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./html:/usr/share/nginx/html:ro&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose is direct and productive. It is especially good when the unit of thought is "this application has several containers." For a comprehensive reference of Compose commands and patterns, see the &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-cheatsheet/" rel="noopener noreferrer"&gt;Docker Compose Cheatsheet&lt;/a&gt;. For Docker commands beyond Compose — images, volumes, networks, and cleanup — see the &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-cheatsheet/" rel="noopener noreferrer"&gt;Docker Cheatsheet&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Podman Quadlet Is Good At
&lt;/h2&gt;

&lt;p&gt;Podman Quadlet is a way to define Podman containers using systemd-style files. Instead of writing a full generated systemd service by hand, you write a declarative file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Example web container&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/library/nginx:stable&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080:80&lt;/span&gt;
&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/example/html:/usr/share/nginx/html:ro&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;/etc/containers/systemd/example.container&lt;/code&gt;, then reload systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; example.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status example.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; example.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core appeal of Quadlet: the container becomes a normal Linux service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Philosophical Difference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose Thinks in Stacks
&lt;/h3&gt;

&lt;p&gt;Compose asks: &lt;em&gt;What services make up this application?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A Compose project usually lives near application code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;myapp/
  compose.yaml
  .env
  app/
  db/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You start the project as a unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You update the project as a unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is simple, visible, and portable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Podman Quadlet Thinks in Services
&lt;/h3&gt;

&lt;p&gt;Quadlet asks: &lt;em&gt;What containers should this Linux host run as services?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Quadlet files live in systemd-related container paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/containers/systemd/
~/.config/containers/systemd/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You manage generated services with systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status myapp.service
systemctl restart myapp.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This feels more native on a Linux server. For general systemd service patterns, see &lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/executable-as-a-service-in-linux/" rel="noopener noreferrer"&gt;Run any Executable as a Service in Linux&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Important Difference: Who Owns Lifecycle?
&lt;/h2&gt;

&lt;p&gt;With Docker Compose, Compose owns the application lifecycle. With Quadlet, systemd owns the service lifecycle.&lt;/p&gt;

&lt;p&gt;This affects boot behavior, shutdown behavior, restart policy, dependency ordering, logs, health visibility, user services, updates, integration with timers, and integration with other host services.&lt;/p&gt;

&lt;p&gt;If you already use systemd to manage everything else on the host, Quadlet fits neatly. If you think mainly in terms of application stacks, Compose is usually more comfortable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Compose Under systemd vs Quadlet
&lt;/h2&gt;

&lt;p&gt;You can run Docker Compose as a systemd service. That is often a good pattern. Example systemd unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;MyApp Docker Compose stack&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/myapp&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecReload&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose down&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well. But it is still a wrapper around Compose. systemd starts the Compose command, while Docker and Compose handle containers behind it.&lt;/p&gt;

&lt;p&gt;With Quadlet, the unit generation is designed for Podman and systemd directly. You write container-oriented unit files, and systemd manages the generated services.&lt;/p&gt;

&lt;p&gt;The distinction is subtle but important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker Compose under systemd:
  systemd manages a Compose command.

Podman Quadlet:
  systemd manages generated container services.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a detailed walkthrough of this pattern, see &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-as-systemd-service/" rel="noopener noreferrer"&gt;Run Docker Compose as a Linux Service with systemd&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  File Format Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose YAML
&lt;/h3&gt;

&lt;p&gt;Compose uses YAML. It is compact, popular, and easy to share. It is also indentation-sensitive and can grow messy when a stack becomes large.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/example/app:1.0.0&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;APP_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-data:/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Quadlet Unit Files
&lt;/h3&gt;

&lt;p&gt;Quadlet uses systemd-like files. They are more verbose when you have many services, but readable if you already understand systemd.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Example app container&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ghcr.io/example/app:1.0.0&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080:8080&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;APP_ENV=production&lt;/span&gt;
&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app-data.volume:/data&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a volume file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Volume]&lt;/span&gt;
&lt;span class="py"&gt;VolumeName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app-data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saved as &lt;code&gt;app.container&lt;/code&gt; and &lt;code&gt;app-data.volume&lt;/code&gt; in the appropriate systemd container directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping Docker Compose Concepts to Quadlet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Compose concept&lt;/th&gt;
&lt;th&gt;Quadlet equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;services&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.container&lt;/code&gt; files or &lt;code&gt;.pod&lt;/code&gt; plus &lt;code&gt;.container&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;volumes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.volume&lt;/code&gt; files or bind mounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;networks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.network&lt;/code&gt; files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ports&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PublishPort=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Environment=&lt;/code&gt; or &lt;code&gt;EnvironmentFile=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[Service] Restart=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;depends_on&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;systemd &lt;code&gt;After=&lt;/code&gt;, &lt;code&gt;Wants=&lt;/code&gt;, &lt;code&gt;Requires=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;healthcheck&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Podman healthcheck options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;profiles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;systemd enablement and separate units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;journalctl -u service&lt;/code&gt; and &lt;code&gt;podman logs&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl start service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl stop service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;project directory&lt;/td&gt;
&lt;td&gt;systemd container unit directory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The migration is conceptually simple but not mechanical. Compose describes a stack. Quadlet describes services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rootless Containers
&lt;/h2&gt;

&lt;p&gt;Rootless containers are one of the strongest reasons to look at Podman and Quadlet.&lt;/p&gt;

&lt;p&gt;With Docker, many users add themselves to the &lt;code&gt;docker&lt;/code&gt; group. That is convenient, but access to the Docker daemon is effectively powerful host access. On a personal workstation, that may be acceptable. On shared servers, it deserves more caution.&lt;/p&gt;

&lt;p&gt;Podman was designed with rootless usage as a first-class workflow. A rootless Quadlet lives under the user's config directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.config/containers/systemd/whoami.container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then manage it with user systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; whoami.service
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status whoami.service
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; whoami.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To allow the user service to keep running after logout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a clean model for user-owned services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rootless Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Docker Compose&lt;/th&gt;
&lt;th&gt;Podman Quadlet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default common setup&lt;/td&gt;
&lt;td&gt;Rootful Docker daemon&lt;/td&gt;
&lt;td&gt;Rootless-friendly Podman&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User service model&lt;/td&gt;
&lt;td&gt;Possible, but less native&lt;/td&gt;
&lt;td&gt;Native with &lt;code&gt;systemctl --user&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daemon access risk&lt;/td&gt;
&lt;td&gt;Docker socket is powerful&lt;/td&gt;
&lt;td&gt;No central root daemon by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low port binding&lt;/td&gt;
&lt;td&gt;Simple as rootful Docker&lt;/td&gt;
&lt;td&gt;Needs extra setup when rootless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host integration&lt;/td&gt;
&lt;td&gt;Very common&lt;/td&gt;
&lt;td&gt;More Linux-native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared server fit&lt;/td&gt;
&lt;td&gt;Needs care&lt;/td&gt;
&lt;td&gt;Strong fit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rootless is not magic. It has tradeoffs around networking, privileged behavior, and low ports. But for long-running user-owned services, Quadlet is a very elegant model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Startup and Boot Behavior
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose
&lt;/h3&gt;

&lt;p&gt;Compose by itself does not create a boot service. You usually rely on Docker restart policies, a systemd wrapper around &lt;code&gt;docker compose up -d&lt;/code&gt;, a deployment script, or a higher-level tool.&lt;/p&gt;

&lt;p&gt;Example Compose restart policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A systemd wrapper gives you a host-level service. That is good, but it is still an extra wrapper. See &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-as-systemd-service/" rel="noopener noreferrer"&gt;Run Docker Compose as a Linux Service with systemd&lt;/a&gt; for the full pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Podman Quadlet
&lt;/h3&gt;

&lt;p&gt;Quadlet is already systemd-oriented. Enable on boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For rootless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;myapp.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boot behavior is not an add-on. It is the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restart Behavior
&lt;/h2&gt;

&lt;p&gt;Compose commonly uses &lt;code&gt;restart: unless-stopped&lt;/code&gt; in YAML. Quadlet commonly uses systemd restart behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This moves restart logic into the service manager. The preference: use Compose/Docker restart policies for Compose stacks, use systemd restart policies for Quadlet, and do not stack too many supervisors. Keep one clear owner of restart behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose Logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is excellent for developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quadlet Logs
&lt;/h3&gt;

&lt;p&gt;Quadlet services use systemd logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; app.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For rootless units:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; app.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can still use Podman logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman logs &lt;span class="nt"&gt;-f&lt;/span&gt; container-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For server operations, &lt;code&gt;journalctl&lt;/code&gt; integration is a major advantage. Your containers fit into the same log workflow as other Linux services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Updating Docker Compose
&lt;/h3&gt;

&lt;p&gt;A common update flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Easy to wrap in a script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp

docker compose config &lt;span class="nt"&gt;--quiet&lt;/span&gt;
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Updating Podman Quadlet
&lt;/h3&gt;

&lt;p&gt;A simplified manual flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;podman pull ghcr.io/example/app:1.0.1
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart app.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or for rootless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman pull ghcr.io/example/app:1.0.1
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; restart app.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Podman can support auto-update workflows when containers are configured with the right labels and image policy. Quadlet's advantage is not that updates are always simpler. The advantage is that updates are service-manager-native.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-Update Philosophy
&lt;/h2&gt;

&lt;p&gt;Auto-updates are convenient. They are also a risk. For low-risk homelab services, automatic container updates can be fine. For databases, stateful apps, or business services, the preferred flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Back up.&lt;/li&gt;
&lt;li&gt;Pull.&lt;/li&gt;
&lt;li&gt;Recreate or restart.&lt;/li&gt;
&lt;li&gt;Check health.&lt;/li&gt;
&lt;li&gt;Prune later.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compose makes this explicit. Quadlet and Podman can make it systemd-native. Neither tool removes the need for a rollback plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Volumes and Persistent Data
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Compose Volumes
&lt;/h3&gt;

&lt;p&gt;Compose supports named volumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db-data:/var/lib/postgresql/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And bind mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config:/config:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Quadlet Volumes
&lt;/h3&gt;

&lt;p&gt;Quadlet can use bind mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/app/config:/config:ro&lt;/span&gt;
&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/app/data:/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or a &lt;code&gt;.volume&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Volume]&lt;/span&gt;
&lt;span class="py"&gt;VolumeName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app-data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;app-data.volume:/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose is more compact for stack-level storage. Quadlet is more aligned with independently managed service units.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets and Environment Files
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Compose
&lt;/h3&gt;

&lt;p&gt;Compose often uses &lt;code&gt;env_file&lt;/code&gt; or &lt;code&gt;environment&lt;/code&gt; in YAML. For a small private service, &lt;code&gt;.env&lt;/code&gt; is common. For serious systems, treat &lt;code&gt;.env&lt;/code&gt; as sensitive and keep it out of Git.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quadlet
&lt;/h3&gt;

&lt;p&gt;Quadlet can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;APP_ENV=production&lt;/span&gt;
&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/app/app.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restrict permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /opt/app/app.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither Compose nor Quadlet is a complete secret-management system by itself. Do not confuse "not in the command line" with "secure".&lt;/p&gt;

&lt;h2&gt;
  
  
  Networking
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Compose Networking
&lt;/h3&gt;

&lt;p&gt;Compose creates a default project network and gives services DNS names based on service names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app can usually reach the database at &lt;code&gt;db&lt;/code&gt;. Multi-container app networking feels natural. This is one of Compose's strongest features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quadlet Networking
&lt;/h3&gt;

&lt;p&gt;Quadlet can define networks separately with &lt;code&gt;.network&lt;/code&gt; files or use Podman network options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Network]&lt;/span&gt;
&lt;span class="py"&gt;NetworkName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;appnet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Container file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;example/app:stable&lt;/span&gt;
&lt;span class="py"&gt;Network&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;appnet.network&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is more explicit and systemd-like. For one or two containers, it is fine. For a large app stack, Compose is often easier to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pods
&lt;/h2&gt;

&lt;p&gt;Podman has a native pod concept. That matters if you like the Kubernetes mental model where multiple containers share a network namespace and lifecycle boundary. Quadlet supports &lt;code&gt;.pod&lt;/code&gt; files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Pod]&lt;/span&gt;
&lt;span class="py"&gt;PodName&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080:8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A container can join that pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ghcr.io/example/app:stable&lt;/span&gt;
&lt;span class="py"&gt;Pod&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp.pod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose does not have the same pod model. It has services on networks. For most simple web apps, Compose networks are enough. For Podman users who like pod-style grouping, Quadlet is a better match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build Workflows
&lt;/h2&gt;

&lt;p&gt;Compose is usually better when you build images as part of the local application workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is extremely convenient for development. Quadlet is usually better when you run already-built images as services. Build images with Podman separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman build &lt;span class="nt"&gt;-t&lt;/span&gt; localhost/myapp:latest &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;localhost/myapp:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your workflow is "edit code, rebuild, restart stack", Compose wins. If your workflow is "deploy a known image as a Linux service", Quadlet wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Portability
&lt;/h2&gt;

&lt;p&gt;Compose files are widely shared. Many open-source projects provide a &lt;code&gt;compose.yaml&lt;/code&gt; or &lt;code&gt;docker-compose.yml&lt;/code&gt;. If a project says "run this with Docker Compose", you can usually start quickly with &lt;code&gt;docker compose up -d&lt;/code&gt;. This is a major practical advantage.&lt;/p&gt;

&lt;p&gt;Quadlet is portable across systems that have Podman, systemd, and compatible Quadlet support. That is a narrower target, but a very good one for modern Linux servers. Quadlet is not the best format for sharing an application with every possible developer. It is a good format for describing how a specific Linux host should run a containerized service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer Experience
&lt;/h2&gt;

&lt;p&gt;Docker Compose usually wins developer experience. More examples, more tutorials, more project templates, easier local builds, easy one-file stack, familiar &lt;code&gt;docker compose up&lt;/code&gt;, and strong fit for dev dependencies. A developer can read this quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quadlet can do similar things, but it is more operations-shaped. For local development, I would rarely start with Quadlet unless the application itself is specifically about Podman or systemd.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operations Experience
&lt;/h2&gt;

&lt;p&gt;Quadlet often wins operations experience on a Linux host. Reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native &lt;code&gt;systemctl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Native &lt;code&gt;journalctl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Rootless user services&lt;/li&gt;
&lt;li&gt;systemd dependencies&lt;/li&gt;
&lt;li&gt;systemd timers&lt;/li&gt;
&lt;li&gt;systemd restart behavior&lt;/li&gt;
&lt;li&gt;No central Docker daemon&lt;/li&gt;
&lt;li&gt;Better fit with host service management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A server admin can reason about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status app.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; app.service &lt;span class="nt"&gt;-f&lt;/span&gt;
systemctl restart app.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the normal Linux service workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Model
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose Security Notes
&lt;/h3&gt;

&lt;p&gt;Docker Compose usually talks to the Docker daemon. On a normal Linux Docker install, access to the Docker socket is powerful. A user who can control Docker can often mount host paths, run privileged containers, or otherwise gain broad host control. For installation options including rootless Docker on Ubuntu, see &lt;a href="https://www.glukhov.org/developer-tools/containers/install-docker-on-ubuntu/" rel="noopener noreferrer"&gt;Install Docker on Ubuntu&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Practical advice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not casually expose &lt;code&gt;/var/run/docker.sock&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Treat the &lt;code&gt;docker&lt;/code&gt; group as privileged.&lt;/li&gt;
&lt;li&gt;Avoid privileged containers.&lt;/li&gt;
&lt;li&gt;Avoid host mounts unless needed.&lt;/li&gt;
&lt;li&gt;Keep secrets out of Git.&lt;/li&gt;
&lt;li&gt;Use explicit image tags for important services.&lt;/li&gt;
&lt;li&gt;Review published ports.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quadlet Security Notes
&lt;/h3&gt;

&lt;p&gt;Podman Quadlet pairs well with rootless containers and user systemd services. This can reduce risk, especially on shared hosts or personal servers where services should not require a root daemon.&lt;/p&gt;

&lt;p&gt;Practical advice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer rootless services when they fit.&lt;/li&gt;
&lt;li&gt;Use user units for user-owned services.&lt;/li&gt;
&lt;li&gt;Use system units only when host-level privileges are needed.&lt;/li&gt;
&lt;li&gt;Avoid unnecessary privileged containers.&lt;/li&gt;
&lt;li&gt;Keep environment files locked down.&lt;/li&gt;
&lt;li&gt;Think carefully about bind mounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rootless does not mean risk-free. It means the default blast radius can be smaller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;For most web services, internal tools, and self-hosted apps, performance is not the deciding factor. The main differences are operational, not raw speed. Choose based on lifecycle model, security model, host integration, team familiarity, update process, networking needs, and debugging workflow.&lt;/p&gt;

&lt;p&gt;If you are choosing between Compose and Quadlet because of performance alone, you are probably optimizing the wrong layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Docker Compose Failure Modes
&lt;/h3&gt;

&lt;p&gt;Common problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker daemon not running&lt;/li&gt;
&lt;li&gt;Compose plugin missing&lt;/li&gt;
&lt;li&gt;Wrong project directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; not loaded as expected&lt;/li&gt;
&lt;li&gt;Old &lt;code&gt;docker-compose&lt;/code&gt; binary used by accident&lt;/li&gt;
&lt;li&gt;Containers not recreated after config changes&lt;/li&gt;
&lt;li&gt;Orphan containers left after service rename&lt;/li&gt;
&lt;li&gt;Volumes deleted with &lt;code&gt;down -v&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Docker logs filling the disk&lt;/li&gt;
&lt;li&gt;Docker socket permission errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best fixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose config
docker compose ps
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Podman Quadlet Failure Modes
&lt;/h3&gt;

&lt;p&gt;Common problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit file in the wrong directory&lt;/li&gt;
&lt;li&gt;Forgot &lt;code&gt;systemctl daemon-reload&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Using system units when user units were intended&lt;/li&gt;
&lt;li&gt;Forgot &lt;code&gt;loginctl enable-linger&lt;/code&gt; for rootless services&lt;/li&gt;
&lt;li&gt;Image pull takes longer than systemd startup timeout&lt;/li&gt;
&lt;li&gt;cgroup v2 not available&lt;/li&gt;
&lt;li&gt;SELinux labels or volume permissions&lt;/li&gt;
&lt;li&gt;Service name differs from file expectations&lt;/li&gt;
&lt;li&gt;Network or volume unit not enabled or referenced correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best fixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status app.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; app.service &lt;span class="nt"&gt;-f&lt;/span&gt;
systemctl daemon-reload
podman ps &lt;span class="nt"&gt;-a&lt;/span&gt;
podman logs container-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For rootless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status app.service
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; app.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Migration Example: Compose to Quadlet
&lt;/h2&gt;

&lt;p&gt;Start with this Compose service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whoami&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik/whoami:v1.10&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;WHOAMI_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;compose-demo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rough Quadlet equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Whoami demo container&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Container]&lt;/span&gt;
&lt;span class="py"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.io/traefik/whoami:v1.10&lt;/span&gt;
&lt;span class="py"&gt;PublishPort&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080:80&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;WHOAMI_NAME=quadlet-demo&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save as &lt;code&gt;/etc/containers/systemd/whoami.container&lt;/code&gt;, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; whoami.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status whoami.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example is easy because it is one container. A larger Compose stack with databases, networks, volumes, and build steps needs more careful translation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Checklist
&lt;/h2&gt;

&lt;p&gt;Before moving from Compose to Quadlet, ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Is this stack really a set of long-running host services?
[ ] Are the images already built and published?
[ ] Do I need rootless services?
[ ] Do I want systemd dependencies and timers?
[ ] Are volumes and bind mounts clearly understood?
[ ] Are ports documented?
[ ] Are secrets handled outside Git?
[ ] Is there a backup and restore process?
[ ] Can I monitor logs through journalctl?
[ ] Do I have a rollback path?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If most answers are yes, Quadlet may be a good fit. If the stack is mostly for local development, Compose is probably still better.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Stay with Docker Compose
&lt;/h2&gt;

&lt;p&gt;Stay with Compose when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The project already ships a good Compose file.&lt;/li&gt;
&lt;li&gt;You need the easiest onboarding path.&lt;/li&gt;
&lt;li&gt;Developers run the same stack locally.&lt;/li&gt;
&lt;li&gt;You build images during development.&lt;/li&gt;
&lt;li&gt;You want one YAML file for services, volumes, and networks.&lt;/li&gt;
&lt;li&gt;You want maximum tutorial and community compatibility.&lt;/li&gt;
&lt;li&gt;Your current systemd wrapper works fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no prize for migrating a working Compose stack to Quadlet just because Quadlet is cleaner in theory. If Compose is boring and reliable for your use case, keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Move to Podman Quadlet
&lt;/h2&gt;

&lt;p&gt;Move to Quadlet when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The stack is really a host service.&lt;/li&gt;
&lt;li&gt;You want rootless service management.&lt;/li&gt;
&lt;li&gt;You prefer Podman over Docker.&lt;/li&gt;
&lt;li&gt;You want systemd to own lifecycle.&lt;/li&gt;
&lt;li&gt;You want &lt;code&gt;journalctl&lt;/code&gt; logs.&lt;/li&gt;
&lt;li&gt;You want service dependencies.&lt;/li&gt;
&lt;li&gt;You want user services that survive logout.&lt;/li&gt;
&lt;li&gt;You want less Docker daemon exposure.&lt;/li&gt;
&lt;li&gt;You are building a self-hosting host around systemd.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quadlet is not "Compose but better." It is a different design center.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern 1: Local Development
&lt;/h3&gt;

&lt;p&gt;Use Docker Compose. Fast, familiar, portable, easy to rebuild, easy for teams.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Single-Host Self-Hosting
&lt;/h3&gt;

&lt;p&gt;Use either. Choose Compose if the project already provides a Compose file. Choose Quadlet if you want systemd-native service management. Compose gives a better app bundle; Quadlet gives a better Linux service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 3: User-Owned Rootless Service
&lt;/h3&gt;

&lt;p&gt;Use Podman Quadlet. Rootless workflow, user-level service management, no central Docker daemon.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.config/containers/systemd/app.container
systemctl --user enable --now app.service
loginctl enable-linger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 4: Production-Like Single Server
&lt;/h3&gt;

&lt;p&gt;Use Docker Compose with a disciplined systemd wrapper, or use Quadlet if your team is comfortable with Podman. Do not choose based on fashion. Choose based on who will operate it at 2 AM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 5: Multi-Node Platform
&lt;/h3&gt;

&lt;p&gt;Use neither as the final orchestration layer. Consider Kubernetes, Nomad, Swarm, or a managed platform. Compose and Quadlet are excellent single-host tools. They are not cluster schedulers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Decision Tree
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is this mainly for local development?
  yes:
    use Docker Compose
  no:
    continue

Does the project already provide a maintained compose.yaml?
  yes:
    use Docker Compose unless you have a strong reason to migrate
  no:
    continue

Do you want rootless long-running services managed by systemd?
  yes:
    use Podman Quadlet
  no:
    continue

Do you want the easiest multi-container app definition?
  yes:
    use Docker Compose
  no:
    continue

Do you want containers to behave like normal Linux services?
  yes:
    use Podman Quadlet
  no:
    use Docker Compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Side-by-Side Commands
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Docker Compose&lt;/th&gt;
&lt;th&gt;Podman Quadlet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl start app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl stop app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restart&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl restart app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apply changes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl daemon-reload &amp;amp;&amp;amp; systemctl restart app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose logs -f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;journalctl -u app.service -f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose ps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl status app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable on boot&lt;/td&gt;
&lt;td&gt;systemd wrapper or restart policy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl enable app.service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pull update&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose pull&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;podman pull image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rootless service&lt;/td&gt;
&lt;td&gt;possible&lt;/td&gt;
&lt;td&gt;natural with &lt;code&gt;systemctl --user&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Common Misunderstandings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "Quadlet Replaces Docker Compose"
&lt;/h3&gt;

&lt;p&gt;Not exactly. Quadlet replaces some Compose use cases, especially long-running Linux services. It does not replace Compose as the easiest application-stack format for developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Docker Compose Is Not Production Ready"
&lt;/h3&gt;

&lt;p&gt;Too broad. Compose can be perfectly reasonable for small production systems if you understand backups, updates, logging, restart behavior, and host security. The problem is not Compose. The problem is pretending a single-host Compose deployment has the same properties as a cluster orchestrator.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Podman Is Just Docker Without the Daemon"
&lt;/h3&gt;

&lt;p&gt;Too simple. Podman has Docker-compatible commands, but its design center is different: daemonless operation, rootless workflows, pods, and Linux integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Rootless Means Secure"
&lt;/h3&gt;

&lt;p&gt;No. Rootless reduces some risks. It does not make bad images, exposed secrets, unsafe bind mounts, or vulnerable apps safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  "systemd Is Too Heavy for Containers"
&lt;/h3&gt;

&lt;p&gt;systemd is already the service manager on most mainstream Linux servers. Using it to manage long-running containers is not strange. It is often the boring and correct thing to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Recommendation
&lt;/h2&gt;

&lt;p&gt;Use Docker Compose when the application stack is the main thing. Use Podman Quadlet when the Linux service is the main thing.&lt;/p&gt;

&lt;p&gt;That distinction is more useful than arguing which tool is better. For developer workflows, Compose is hard to beat. It is popular, readable, portable, and supported by countless projects. For long-running Linux services, Quadlet is quietly excellent. It makes containers feel like native systemd services, works naturally with rootless Podman, and fits the operational model of a serious Linux host.&lt;/p&gt;

&lt;p&gt;The preferred split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Local development: Docker Compose
Portable app examples: Docker Compose
Small self-hosted stacks: Docker Compose or Quadlet
Rootless user services: Podman Quadlet
Long-running host services: Podman Quadlet
Multi-node orchestration: neither; use a real orchestrator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not migrate just to be modern. Migrate when the lifecycle model is better. Compose is a great stack tool. Quadlet is a great service tool. The smart choice is to use each where its mental model matches the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.podman.io/en/latest/markdown/podman-quadlet.1.html" rel="noopener noreferrer"&gt;Podman Quadlet Documentation&lt;/a&gt; — official Podman Quadlet reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose Documentation&lt;/a&gt; — official Docker Compose reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-as-systemd-service/" rel="noopener noreferrer"&gt;Run Docker Compose as a Linux Service with systemd&lt;/a&gt; — detailed Compose-as-systemd walkthrough on this site&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-cheatsheet/" rel="noopener noreferrer"&gt;Docker Compose Cheatsheet&lt;/a&gt; — Compose commands and patterns reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/containers/docker-cheatsheet/" rel="noopener noreferrer"&gt;Docker Cheatsheet&lt;/a&gt; — Docker commands reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/containers/install-docker-on-ubuntu/" rel="noopener noreferrer"&gt;Install Docker on Ubuntu&lt;/a&gt; — Docker installation guide with rootless alternatives&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/executable-as-a-service-in-linux/" rel="noopener noreferrer"&gt;Run any Executable as a Service in Linux&lt;/a&gt; — general systemd service patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools: The Complete Guide to Modern Development Workflows&lt;/a&gt; — cluster home&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>linux</category>
      <category>devops</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Hermes Agent: Headless Server + Remote Desktop Setup</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:07:27 +0000</pubDate>
      <link>https://dev.to/rosgluk/hermes-agent-headless-server-remote-desktop-setup-3dhd</link>
      <guid>https://dev.to/rosgluk/hermes-agent-headless-server-remote-desktop-setup-3dhd</guid>
      <description>&lt;p&gt;Running Hermes Agent on a headless server while connecting from a desktop client on another machine requires two server processes and a single client connection.&lt;/p&gt;

&lt;p&gt;The architecture separates the Hermes backend into two server-side processes and one client-side surface. The &lt;code&gt;hermes serve&lt;/code&gt; backend handles the API and dashboard connections, while the &lt;code&gt;hermes gateway run&lt;/code&gt; process manages messaging channels independently. The desktop client connects to the serve backend, not the gateway.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    subgraph Server["HEADLESS SERVER"]
        serve["hermes serve&amp;lt;br/&amp;gt;--host 0.0.0.0&amp;lt;br/&amp;gt;:9119"]
        gateway["hermes gateway run&amp;lt;br/&amp;gt;Telegram, Discord, Slack"]
    end

    subgraph Client["DESKTOP PC"]
        desktop["hermes desktop&amp;lt;br/&amp;gt;(WebSocket connection)"]
    end

    desktop &amp;lt;---&amp;gt;|WebSocket| serve
    gateway -.-&amp;gt;|Shares ~/.hermes/| serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two processes on the server, one app on the client. Both server processes share the same &lt;code&gt;~/.hermes/&lt;/code&gt; config, skills, memory, and sessions. Cron jobs execute on the server where the gateway runs.&lt;/p&gt;

&lt;p&gt;For installation, provider setup, and initial configuration, start with the &lt;a href="https://www.glukhov.org/ai-systems/hermes/" rel="noopener noreferrer"&gt;Hermes AI Assistant — Install, Setup, Workflow, and Troubleshooting&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up the headless server
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Configure authentication
&lt;/h3&gt;

&lt;p&gt;Basic auth provides sufficient protection for a trusted LAN. Add credentials to the environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.hermes/.env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
HERMES_DASHBOARD_BASIC_AUTH_USERNAME=admin
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=choose-a-strong-password
HERMES_DASHBOARD_BASIC_AUTH_SECRET=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.hermes/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file lives alongside &lt;code&gt;config.yaml&lt;/code&gt; under &lt;code&gt;~/.hermes/&lt;/code&gt;. Hermes resolves configuration with CLI overrides first, then &lt;code&gt;config.yaml&lt;/code&gt;, then &lt;code&gt;.env&lt;/code&gt;, then built-in defaults. Secrets belong in &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Start the backend
&lt;/h3&gt;

&lt;p&gt;Run the serve backend on all interfaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hermes serve &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 9119
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend listens on port 9119 and accepts WebSocket connections from the desktop client. Verify it is running with a quick status check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:9119/api/status | jq &lt;span class="s1"&gt;'.auth_required, .auth_providers'&lt;/span&gt;
&lt;span class="c"&gt;# Expected: true  "basic"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Run as a systemd service
&lt;/h3&gt;

&lt;p&gt;For a persistent server that survives reboots, install a user-level systemd service. Create the unit file at &lt;code&gt;/etc/systemd/user/hermes-serve.service&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Hermes Agent Serve Backend&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%h/.hermes/.env&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/rg/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main serve --host 0.0.0.0 --port 9119&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the daemon, enable the service, and start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;hermes-serve
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start hermes-serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the service status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status hermes-serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Start the gateway
&lt;/h3&gt;

&lt;p&gt;The gateway is a separate process that handles messaging channels — Telegram, Discord, Slack, and others. Start it independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hermes gateway run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway and the serve backend are two separate processes. The gateway manages sessions, runs cron jobs, and routes messages. The serve backend provides the API surface for desktop and web dashboard connections. They share the same home directory but run independently.&lt;/p&gt;

&lt;p&gt;For the full list of gateway commands and subcommands, see the &lt;a href="https://www.glukhov.org/ai-systems/hermes/hermes-agent-cli-cheatsheet/" rel="noopener noreferrer"&gt;Hermes Agent CLI cheat sheet&lt;/a&gt;.&lt;br&gt;
If your primary interface is mobile messaging, pair this setup with &lt;a href="https://www.glukhov.org/ai-systems/hermes/hermes-voice-control/" rel="noopener noreferrer"&gt;Hermes Voice Control from Your Phone&lt;/a&gt; for voice-first workflows on top of the same gateway process.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Verify the backend
&lt;/h3&gt;

&lt;p&gt;Confirm the backend is responding and authentication is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:9119/api/status | jq &lt;span class="s1"&gt;'.auth_required, .auth_providers'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;true
"basic"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If authentication is not enabled, the response will show &lt;code&gt;false&lt;/code&gt; for &lt;code&gt;auth_required&lt;/code&gt;. Check that the &lt;code&gt;.env&lt;/code&gt; file contains the correct variables and that the service has restarted after configuration changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect from the desktop client
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A: Hermes Desktop App
&lt;/h3&gt;

&lt;p&gt;Install Hermes Desktop from the &lt;a href="https://hermes-agent.nousresearch.com/" rel="noopener noreferrer"&gt;official site&lt;/a&gt;. Launch the app, navigate to &lt;strong&gt;Settings → Gateway → Remote gateway&lt;/strong&gt;, and enter the server address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://&amp;lt;server-ip&amp;gt;:9119
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sign in with the username and password you configured on the server.&lt;/p&gt;

&lt;p&gt;Alternatively, set the remote URL via environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;HERMES_DESKTOP_REMOTE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://&amp;lt;server-ip&amp;gt;:9119 hermes desktop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option B: Web dashboard
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;http://&amp;lt;server-ip&amp;gt;:9119&lt;/code&gt; in a browser and sign in with the basic auth credentials. The web dashboard provides a browser-based interface to the Hermes backend without requiring a desktop installation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option C: CLI
&lt;/h3&gt;

&lt;p&gt;From the desktop PC's terminal, configure the remote URL via the desktop app settings or the &lt;code&gt;HERMES_DESKTOP_REMOTE_URL&lt;/code&gt; environment variable. The CLI surface connects through the same WebSocket channel as the desktop app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network and security considerations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trusted LAN&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--host 0.0.0.0&lt;/code&gt; + basic auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exposed to internet&lt;/td&gt;
&lt;td&gt;Use Tailscale (&lt;code&gt;--host &amp;lt;tailscale-ip&amp;gt;&lt;/code&gt;) or OAuth provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall&lt;/td&gt;
&lt;td&gt;Open port 9119 TCP on the server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a trusted local network, basic auth on &lt;code&gt;0.0.0.0&lt;/code&gt; is adequate. If the server is exposed to the internet, use Tailscale to bind to the Tailscale IP instead of &lt;code&gt;0.0.0.0&lt;/code&gt;, or configure an OAuth provider for stronger authentication. Always open port 9119 TCP in the server's firewall when the backend needs to accept external connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important process distinctions
&lt;/h2&gt;

&lt;p&gt;Understanding the separation between the two server processes prevents common configuration mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hermes serve&lt;/code&gt;&lt;/strong&gt; — The backend that the desktop app and web dashboard connect to. Handles the API surface and WebSocket connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hermes gateway run&lt;/code&gt;&lt;/strong&gt; — The process that handles Telegram, Discord, Slack, and other messaging channels. Manages sessions, runs cron jobs, and routes messages.&lt;/li&gt;
&lt;li&gt;These are &lt;strong&gt;two separate processes&lt;/strong&gt; on the server. They share &lt;code&gt;~/.hermes/&lt;/code&gt; config, skills, memory, and sessions, but run independently.&lt;/li&gt;
&lt;li&gt;Cron jobs execute on the server where the gateway runs.&lt;/li&gt;
&lt;li&gt;Profiles, skills, and memory are configured on the server side. The client connects to the already-configured backend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For profile-first configuration and skills tuned to different production roles, see &lt;a href="https://www.glukhov.org/ai-systems/hermes/production-setup/" rel="noopener noreferrer"&gt;Hermes AI Assistant Skills for Real Production Setups&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Desktop cannot connect to the server
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Verify the backend is running: &lt;code&gt;systemctl --user status hermes-serve&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check the port is open: &lt;code&gt;ss -tlnp | grep 9119&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test from the server: &lt;code&gt;curl -s http://localhost:9119/api/status&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test from the client machine: &lt;code&gt;curl -s http://&amp;lt;server-ip&amp;gt;:9119/api/status&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If the client test fails, check firewall rules: &lt;code&gt;sudo ufw status&lt;/code&gt; or &lt;code&gt;sudo iptables -L&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Authentication fails
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Confirm &lt;code&gt;.env&lt;/code&gt; file has correct permissions: &lt;code&gt;ls -la ~/.hermes/.env&lt;/code&gt; (should be &lt;code&gt;600&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Verify the secret was generated: &lt;code&gt;grep HERMES_DASHBOARD_BASIC_AUTH_SECRET ~/.hermes/.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Restart the service after config changes: &lt;code&gt;systemctl --user restart hermes-serve&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Gateway does not respond to messages
&lt;/h3&gt;

&lt;p&gt;The gateway is a separate process from the backend. If the desktop connects but messaging platforms do not work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check gateway status: &lt;code&gt;hermes gateway status&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Start the gateway if stopped: &lt;code&gt;hermes gateway start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Review logs: &lt;code&gt;hermes logs gateway -f&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/desktop" rel="noopener noreferrer"&gt;Hermes Agent Desktop App Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/configuration" rel="noopener noreferrer"&gt;Hermes Agent Configuration Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/messaging" rel="noopener noreferrer"&gt;Hermes Agent Messaging Gateway&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-systems/hermes/" rel="noopener noreferrer"&gt;Hermes AI Assistant — Install, Setup, Workflow, and Troubleshooting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-systems/hermes/hermes-agent-cli-cheatsheet/" rel="noopener noreferrer"&gt;Hermes Agent CLI cheat sheet — commands, flags, and slash shortcuts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glukhov.org/ai-systems/hermes/production-setup/" rel="noopener noreferrer"&gt;Hermes AI Assistant Skills for Real Production Setups&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is part of the &lt;a href="https://www.glukhov.org/ai-systems/" rel="noopener noreferrer"&gt;AI Systems&lt;/a&gt; cluster, which covers self-hosted assistants, retrieval architecture, local LLM infrastructure, and observability.&lt;/p&gt;

</description>
      <category>hermes</category>
      <category>selfhosting</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>GPUs for AI in 2026: NVIDIA, AMD, Intel Compared</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Tue, 14 Jul 2026 00:14:41 +0000</pubDate>
      <link>https://dev.to/rosgluk/gpus-for-ai-in-2026-nvidia-amd-intel-compared-3gam</link>
      <guid>https://dev.to/rosgluk/gpus-for-ai-in-2026-nvidia-amd-intel-compared-3gam</guid>
      <description>&lt;p&gt;The AI hardware landscape has shifted significantly in 2026, with NVIDIA, AMD, and Intel all competing for developers who need GPUs capable of running local large language models and AI inference workloads.&lt;/p&gt;

&lt;p&gt;Choosing the right GPU for AI workloads requires looking beyond marketing numbers and focusing on the specifications that actually affect real-world performance. Memory capacity, memory bandwidth, and software ecosystem maturity consistently matter more than theoretical compute peaks when running transformer models locally.&lt;/p&gt;

&lt;p&gt;This comparison covers the most relevant workstation and prosumer GPUs available in mid-2026, including NVIDIA's Blackwell architecture (RTX 50-series), AMD's Radeon AI Pro R9700, and Intel's Arc Pro B70. The goal is to provide a practical reference for developers deciding which hardware best fits their model sizes, software stack, and budget constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which GPU specifications matter for AI workloads
&lt;/h2&gt;

&lt;p&gt;Marketing materials from GPU vendors emphasise AI TOPS and tensor performance, but these metrics rarely tell the complete story for local inference. The specifications below are ranked by their actual impact on running large language models.&lt;/p&gt;

&lt;h3&gt;
  
  
  VRAM capacity
&lt;/h3&gt;

&lt;p&gt;VRAM is typically the first limiting factor when running LLMs locally. A model cannot execute entirely on the GPU if it does not fit into available memory. Once model weights spill into system RAM, inference performance drops dramatically.&lt;/p&gt;

&lt;p&gt;Approximate VRAM requirements for common model sizes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model Size&lt;/th&gt;
&lt;th&gt;Recommended VRAM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;8-12 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14B&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32B&lt;/td&gt;
&lt;td&gt;24-32 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70B&lt;/td&gt;
&lt;td&gt;48-64 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120B+&lt;/td&gt;
&lt;td&gt;Multiple GPUs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most homelab users, moving from 16 GB to 32 GB of VRAM provides a substantially larger practical benefit than increasing raw compute performance. A 32 GB GPU capable of running an entire model will often outperform a theoretically faster 16 GB GPU forced to offload tensors into system memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory bandwidth
&lt;/h3&gt;

&lt;p&gt;Memory bandwidth determines how quickly model weights can be streamed into compute units. Large transformer models continuously move massive amounts of data between VRAM and processing cores during inference.&lt;/p&gt;

&lt;p&gt;As models grow, bandwidth often becomes the dominant performance bottleneck. A card with higher bandwidth can outperform another GPU with significantly higher theoretical compute performance, particularly during prompt processing phases where the model reads through the entire context window.&lt;/p&gt;

&lt;h3&gt;
  
  
  FP32 compute
&lt;/h3&gt;

&lt;p&gt;FP32 throughput remains useful for scientific computing, simulation, rendering, and some AI preprocessing workloads. Modern inference engines rarely execute entirely in FP32 precision, relying instead on quantised formats like Q4_K_M or Q8_0. FP32 should be considered a secondary metric for AI inference.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI TOPS and tensor performance
&lt;/h3&gt;

&lt;p&gt;Every GPU vendor promotes AI TOPS as a headline number. These values are not directly comparable across vendors. NVIDIA, AMD, and Intel measure AI throughput differently, use different tensor hardware, and apply different assumptions regarding sparsity and numerical precision.&lt;/p&gt;

&lt;p&gt;AI TOPS should be viewed as an indication of peak theoretical capability rather than an expected LLM inference speed. Real-world token generation rates depend on model architecture, quantisation level, context length, and software optimisation — factors that TOPS numbers do not capture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Software ecosystem maturity
&lt;/h3&gt;

&lt;p&gt;Software support often determines whether hardware reaches its full potential. The current ecosystem landscape is approximately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Primary AI Stack&lt;/th&gt;
&lt;th&gt;Maturity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA&lt;/td&gt;
&lt;td&gt;CUDA, TensorRT&lt;/td&gt;
&lt;td&gt;Industry standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMD&lt;/td&gt;
&lt;td&gt;ROCm, HIP, Vulkan&lt;/td&gt;
&lt;td&gt;Solid for PyTorch, llama.cpp, Ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel&lt;/td&gt;
&lt;td&gt;oneAPI, SYCL, OpenVINO&lt;/td&gt;
&lt;td&gt;Improving rapidly, trailing peers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CUDA remains the industry standard with the broadest library support. ROCm has matured significantly over the past two years and now provides a functional experience for PyTorch, llama.cpp, and Ollama on Linux. Intel's oneAPI ecosystem continues to improve but still trails both NVIDIA and AMD in overall software maturity and community adoption.&lt;/p&gt;

&lt;p&gt;For a deeper look at NVIDIA-specific GPU analysis, see &lt;a href="https://www.glukhov.org/llm-performance/benchmarks/comparing-nvidia-gpu-for-ai/" rel="noopener noreferrer"&gt;Comparing NVIDIA GPU Suitability for AI&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete GPU comparison table
&lt;/h2&gt;

&lt;p&gt;The table below compares the most relevant workstation and enthusiast GPUs for AI workloads in 2026.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;VRAM&lt;/th&gt;
&lt;th&gt;Bandwidth&lt;/th&gt;
&lt;th&gt;FP32 (TFLOPS)&lt;/th&gt;
&lt;th&gt;AI TOPS (INT8)&lt;/th&gt;
&lt;th&gt;TBP&lt;/th&gt;
&lt;th&gt;MSRP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX 5090&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;1792 GB/s&lt;/td&gt;
&lt;td&gt;104.6&lt;/td&gt;
&lt;td&gt;3352&lt;/td&gt;
&lt;td&gt;575 W&lt;/td&gt;
&lt;td&gt;$1799&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX 5080&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;960 GB/s&lt;/td&gt;
&lt;td&gt;56.3&lt;/td&gt;
&lt;td&gt;1801&lt;/td&gt;
&lt;td&gt;360 W&lt;/td&gt;
&lt;td&gt;$999&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX 5070 Ti&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;896 GB/s&lt;/td&gt;
&lt;td&gt;43.9&lt;/td&gt;
&lt;td&gt;1406&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;$649&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX 5070&lt;/td&gt;
&lt;td&gt;12 GB&lt;/td&gt;
&lt;td&gt;672 GB/s&lt;/td&gt;
&lt;td&gt;30.9&lt;/td&gt;
&lt;td&gt;494&lt;/td&gt;
&lt;td&gt;250 W&lt;/td&gt;
&lt;td&gt;$549&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX 5060 Ti 16GB&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;448 GB/s&lt;/td&gt;
&lt;td&gt;23.7&lt;/td&gt;
&lt;td&gt;614&lt;/td&gt;
&lt;td&gt;180 W&lt;/td&gt;
&lt;td&gt;$399&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 6000&lt;/td&gt;
&lt;td&gt;96 GB&lt;/td&gt;
&lt;td&gt;1792 GB/s&lt;/td&gt;
&lt;td&gt;125.0&lt;/td&gt;
&lt;td&gt;4000&lt;/td&gt;
&lt;td&gt;600 W&lt;/td&gt;
&lt;td&gt;$4999&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 5000&lt;/td&gt;
&lt;td&gt;48 GB&lt;/td&gt;
&lt;td&gt;1344 GB/s&lt;/td&gt;
&lt;td&gt;73.7&lt;/td&gt;
&lt;td&gt;2064&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;$2499&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 4500&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;896 GB/s&lt;/td&gt;
&lt;td&gt;54.9&lt;/td&gt;
&lt;td&gt;1577&lt;/td&gt;
&lt;td&gt;200 W&lt;/td&gt;
&lt;td&gt;$2500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 4000&lt;/td&gt;
&lt;td&gt;24 GB&lt;/td&gt;
&lt;td&gt;672 GB/s&lt;/td&gt;
&lt;td&gt;46.9&lt;/td&gt;
&lt;td&gt;1178&lt;/td&gt;
&lt;td&gt;145 W&lt;/td&gt;
&lt;td&gt;$1500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 4000 SFF&lt;/td&gt;
&lt;td&gt;24 GB&lt;/td&gt;
&lt;td&gt;432 GB/s&lt;/td&gt;
&lt;td&gt;46.9&lt;/td&gt;
&lt;td&gt;770&lt;/td&gt;
&lt;td&gt;125 W&lt;/td&gt;
&lt;td&gt;$1500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIDIA RTX PRO 2000&lt;/td&gt;
&lt;td&gt;16 GB&lt;/td&gt;
&lt;td&gt;288 GB/s&lt;/td&gt;
&lt;td&gt;18.4&lt;/td&gt;
&lt;td&gt;592&lt;/td&gt;
&lt;td&gt;70 W&lt;/td&gt;
&lt;td&gt;$700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMD Radeon AI Pro R9700&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;640 GB/s&lt;/td&gt;
&lt;td&gt;47.8&lt;/td&gt;
&lt;td&gt;766&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;$1299&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel Arc Pro B70&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;608 GB/s&lt;/td&gt;
&lt;td&gt;22.94&lt;/td&gt;
&lt;td&gt;367&lt;/td&gt;
&lt;td&gt;230 W&lt;/td&gt;
&lt;td&gt;$949&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Key observations by segment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Consumer GPUs
&lt;/h3&gt;

&lt;p&gt;The RTX 5090 remains the fastest single-GPU solution for local AI development, combining exceptional memory bandwidth with the mature CUDA ecosystem. For users running large quantised models, it currently represents the highest-performance consumer option.&lt;/p&gt;

&lt;p&gt;The RTX 5080 and RTX 5070 Ti both offer 16 GB of VRAM, which is sufficient for most 7B-14B models but limits you when working with larger checkpoints. The RTX 5060 Ti 16GB variant is an interesting budget option — 16 GB of VRAM at $399 is compelling for entry-level AI workloads, though the narrower memory bus will impact throughput.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workstation GPUs
&lt;/h3&gt;

&lt;p&gt;Within the workstation segment, AMD's Radeon AI Pro R9700 occupies an attractive middle ground. It delivers 32 GB of VRAM, competitive memory bandwidth, and a significantly lower purchase price than NVIDIA's professional offerings. For developers already comfortable with ROCm on Linux, it provides one of the strongest value propositions in 2026.&lt;/p&gt;

&lt;p&gt;Intel's Arc Pro B70 is particularly interesting because of its pricing. Although it offers lower compute performance than both NVIDIA and AMD, it provides the same 32 GB memory capacity while consuming less power. For users building cost-effective multi-GPU inference servers, the B70 deserves consideration — especially if the oneAPI ecosystem meets your software requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Professional GPUs
&lt;/h3&gt;

&lt;p&gt;NVIDIA's RTX PRO series dominates the professional segment, with the RTX PRO 6000 offering 96 GB of VRAM — unmatched by any competitor. For teams running very large models or multiple concurrent inference workloads, the RTX PRO 6000 and RTX PRO 5000 remain the safest choices, though at a premium price.&lt;/p&gt;

&lt;p&gt;For a real-world performance comparison across different hardware platforms, see &lt;a href="https://www.glukhov.org/llm-performance/benchmarks/dgx-spark-vs-mac-studio-vs-rtx4080/" rel="noopener noreferrer"&gt;NVIDIA DGX Spark vs Mac Studio vs RTX-4080&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical hardware considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Physical dimensions and form factor
&lt;/h3&gt;

&lt;p&gt;GPU size varies significantly across product lines and affects compatibility with your case and cooling solution.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;Approx. Length&lt;/th&gt;
&lt;th&gt;Slots&lt;/th&gt;
&lt;th&gt;Cooler Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;333 mm&lt;/td&gt;
&lt;td&gt;2.7×&lt;/td&gt;
&lt;td&gt;Triple-fan, blower or open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5080&lt;/td&gt;
&lt;td&gt;303 mm&lt;/td&gt;
&lt;td&gt;2.5×&lt;/td&gt;
&lt;td&gt;Dual/triple-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5070 Ti&lt;/td&gt;
&lt;td&gt;280 mm&lt;/td&gt;
&lt;td&gt;2.4×&lt;/td&gt;
&lt;td&gt;Dual-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5070&lt;/td&gt;
&lt;td&gt;245 mm&lt;/td&gt;
&lt;td&gt;2.1×&lt;/td&gt;
&lt;td&gt;Dual-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5060 Ti&lt;/td&gt;
&lt;td&gt;200 mm&lt;/td&gt;
&lt;td&gt;1.8×&lt;/td&gt;
&lt;td&gt;Dual-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMD R9700&lt;/td&gt;
&lt;td&gt;300 mm&lt;/td&gt;
&lt;td&gt;2.5×&lt;/td&gt;
&lt;td&gt;Dual-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel Arc Pro B70&lt;/td&gt;
&lt;td&gt;267 mm&lt;/td&gt;
&lt;td&gt;2.1×&lt;/td&gt;
&lt;td&gt;Single/dual-fan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 6000&lt;/td&gt;
&lt;td&gt;438 mm&lt;/td&gt;
&lt;td&gt;3.5×&lt;/td&gt;
&lt;td&gt;Blower, full-height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 5000&lt;/td&gt;
&lt;td&gt;438 mm&lt;/td&gt;
&lt;td&gt;3.5×&lt;/td&gt;
&lt;td&gt;Blower, full-height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 4000&lt;/td&gt;
&lt;td&gt;267 mm&lt;/td&gt;
&lt;td&gt;2.1×&lt;/td&gt;
&lt;td&gt;Blower, low-profile option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 4000 SFF&lt;/td&gt;
&lt;td&gt;178 mm&lt;/td&gt;
&lt;td&gt;1.5×&lt;/td&gt;
&lt;td&gt;Blower, half-height&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The RTX PRO 6000 and 5000 are significantly longer than consumer cards and require full-height tower cases. The RTX PRO 4000 SFF is one of the few GPUs under 180 mm, making it suitable for compact workstation builds and rack-mounted servers.&lt;/p&gt;

&lt;p&gt;Consumer GPUs (RTX 50-series) use open-air coolers that exhaust heat into the case — adequate case airflow is essential. Workstation GPUs use blower-style coolers that exhaust heat directly out the rear, which is better for multi-GPU configurations and enclosed server environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Power delivery and PSU requirements
&lt;/h3&gt;

&lt;p&gt;TBP (Total Board Power) is the GPU's maximum power draw, but actual system requirements depend on transient spikes and CPU overhead.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;TBP&lt;/th&gt;
&lt;th&gt;Recommended PSU&lt;/th&gt;
&lt;th&gt;Power Connectors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;575 W&lt;/td&gt;
&lt;td&gt;1000 W+&lt;/td&gt;
&lt;td&gt;12V-2x6 (20-pin)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5080&lt;/td&gt;
&lt;td&gt;360 W&lt;/td&gt;
&lt;td&gt;750 W&lt;/td&gt;
&lt;td&gt;12V-2x6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5070 Ti&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;650 W&lt;/td&gt;
&lt;td&gt;8-pin + 8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5070&lt;/td&gt;
&lt;td&gt;250 W&lt;/td&gt;
&lt;td&gt;600 W&lt;/td&gt;
&lt;td&gt;8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5060 Ti&lt;/td&gt;
&lt;td&gt;180 W&lt;/td&gt;
&lt;td&gt;550 W&lt;/td&gt;
&lt;td&gt;8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMD R9700&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;650 W&lt;/td&gt;
&lt;td&gt;8-pin + 8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intel Arc Pro B70&lt;/td&gt;
&lt;td&gt;230 W&lt;/td&gt;
&lt;td&gt;550 W&lt;/td&gt;
&lt;td&gt;8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 6000&lt;/td&gt;
&lt;td&gt;600 W&lt;/td&gt;
&lt;td&gt;1000 W+&lt;/td&gt;
&lt;td&gt;12V-2x6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 5000&lt;/td&gt;
&lt;td&gt;300 W&lt;/td&gt;
&lt;td&gt;650 W&lt;/td&gt;
&lt;td&gt;8-pin + 8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 4000&lt;/td&gt;
&lt;td&gt;145 W&lt;/td&gt;
&lt;td&gt;500 W&lt;/td&gt;
&lt;td&gt;8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 4000 SFF&lt;/td&gt;
&lt;td&gt;125 W&lt;/td&gt;
&lt;td&gt;450 W&lt;/td&gt;
&lt;td&gt;8-pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX PRO 2000&lt;/td&gt;
&lt;td&gt;70 W&lt;/td&gt;
&lt;td&gt;400 W&lt;/td&gt;
&lt;td&gt;PCIe slot only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The RTX 5090 and RTX PRO 6000 both exceed 575W TBP and require the newer 12V-2x6 connector (20-pin). Ensure your PSU supports this connector natively — adapter cables from multiple 8-pin connectors are not recommended for cards above 450W due to transient power spikes that can exceed rated capacity momentarily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thermal characteristics and sustained workloads
&lt;/h3&gt;

&lt;p&gt;AI inference workloads keep the GPU under sustained load, unlike gaming which has variable utilisation. This affects thermal behaviour significantly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RTX 5090 at 575W&lt;/strong&gt;: Expect GPU temperatures of 72-78°C under sustained inference. The higher TBP means more heat dissipation is required — a case with positive static pressure and quality filters is recommended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTX 5080 at 360W&lt;/strong&gt;: Runs cooler, typically 65-72°C. More manageable for standard mid-tower cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workstation GPUs (blower)&lt;/strong&gt;: RTX PRO series exhaust heat directly out the case, keeping case temperatures lower. GPU temperatures may read higher (75-82°C) but this is by design — the blower cooler trades GPU temperature for lower case temperature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-power options&lt;/strong&gt;: RTX PRO 2000 at 70W and RTX PRO 4000 SFF at 125W are suitable for passive or low-fan-speed cooling, making them ideal for always-on inference servers where noise matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For multi-GPU setups, blower-style coolers (workstation GPUs) are strongly preferred over open-air consumer coolers, as the second GPU would otherwise pull hot air from the first.&lt;/p&gt;

&lt;h3&gt;
  
  
  PCIe lanes and bandwidth
&lt;/h3&gt;

&lt;p&gt;GPU performance can be limited by PCIe lane count. A GPU plugged into a x8 or x4 slot will experience reduced memory bandwidth compared to a full x16 connection. For multi-GPU setups, understand how PCIe lanes are distributed across your motherboard. See &lt;a href="https://www.glukhov.org/llm-performance/hardware/llm-performance-and-pci-lanes/" rel="noopener noreferrer"&gt;LLM Performance and PCIe Lanes&lt;/a&gt; for detailed analysis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-GPU setups
&lt;/h3&gt;

&lt;p&gt;When a single GPU cannot fit your model, multi-GPU configurations become necessary. NVIDIA NVLink (where supported) and PCIe-based model parallelism are the primary approaches. The &lt;a href="https://www.glukhov.org/hardware/ai/building-team-ai-infrastructure-on-consumer-hardware/" rel="noopener noreferrer"&gt;AI Infrastructure on Consumer Hardware&lt;/a&gt; guide covers multi-GPU deployment strategies in depth.&lt;/p&gt;

&lt;p&gt;Note that AMD and Intel GPUs have limited multi-GPU inference support in most frameworks. If you plan to scale with multiple GPUs, NVIDIA is currently the only practical option.&lt;/p&gt;

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

&lt;p&gt;There is no universally best GPU for AI workloads. The right choice depends on your software stack, budget, and the size of the models you intend to run.&lt;/p&gt;

&lt;p&gt;NVIDIA's Blackwell family remains the benchmark for inference performance, thanks to outstanding memory bandwidth and the maturity of CUDA and TensorRT. AMD's Radeon AI Pro R9700 has established itself as a compelling workstation option, offering an excellent balance between price, memory capacity, and compute performance. Intel's Arc Pro B70 proves that affordable 32 GB workstation GPUs are now a reality, though its software ecosystem continues to mature.&lt;/p&gt;

&lt;p&gt;The most important lesson from 2026 is that AI hardware should no longer be evaluated using gaming benchmarks. For modern LLM inference, VRAM capacity, memory bandwidth, and software support consistently have a greater impact on real-world performance than theoretical AI TOPS alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/llm-performance/benchmarks/comparing-nvidia-gpu-for-ai/" rel="noopener noreferrer"&gt;Comparing NVIDIA GPU Suitability for AI&lt;/a&gt; — NVIDIA-specific GPU analysis with detailed CUDA core and tensor core comparisons&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/hardware/ai/building-team-ai-infrastructure-on-consumer-hardware/" rel="noopener noreferrer"&gt;AI Infrastructure on Consumer Hardware&lt;/a&gt; — Full-stack guide to deploying self-hosted AI with consumer GPUs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/llm-performance/benchmarks/dgx-spark-vs-mac-studio-vs-rtx4080/" rel="noopener noreferrer"&gt;NVIDIA DGX Spark vs Mac Studio vs RTX-4080&lt;/a&gt; — Real-world Ollama performance benchmarks across hardware platforms&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/llm-performance/hardware/llm-performance-and-pci-lanes/" rel="noopener noreferrer"&gt;LLM Performance and PCIe Lanes&lt;/a&gt; — How PCIe configuration affects LLM inference performance&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/llm-hosting/ollama/ollama-cheatsheet/" rel="noopener noreferrer"&gt;Ollama Cheatsheet&lt;/a&gt; — Command reference and tips for Ollama model serving&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/hardware/gpu/rtx-5880-ada/" rel="noopener noreferrer"&gt;Quadro RTX 5880 Ada Review&lt;/a&gt; — Review of the 48GB workstation GPU alternative&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/llm-performance/benchmarks/best-llm-on-16gb-vram-gpu/" rel="noopener noreferrer"&gt;Best LLM on 16 GB VRAM GPU&lt;/a&gt; — llama.cpp benchmarks for models on 16 GB VRAM&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gpu</category>
      <category>ai</category>
      <category>nvidia</category>
      <category>hardware</category>
    </item>
    <item>
      <title>Spec-Driven Development Workflow From Requirements to Code</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 13 Jul 2026 04:21:33 +0000</pubDate>
      <link>https://dev.to/rosgluk/spec-driven-development-workflow-from-requirements-to-code-3a9j</link>
      <guid>https://dev.to/rosgluk/spec-driven-development-workflow-from-requirements-to-code-3a9j</guid>
      <description>&lt;p&gt;Spec-Driven Development works when the specification is a workflow, not a document you file away after kickoff. The point is not to produce a large product requirements document.&lt;/p&gt;

&lt;p&gt;The point is to move through a sequence of reviewable artifacts that each reduce ambiguity before anyone -- human or AI agent -- changes production code.&lt;/p&gt;

&lt;p&gt;If you do not know what SDD is conceptually, start with &lt;a href="https://www.glukhov.org/app-architecture/documentation/what-is-spec-driven-development/" rel="noopener noreferrer"&gt;What Is Spec-Driven Development?&lt;/a&gt; for definitions, comparisons with TDD and BDD, and the case for treating the spec as source of truth. This article in the &lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;App Architecture&lt;/a&gt; documentation cluster is the operational guide. It walks through the five phases, shows what each artifact should contain, explains where AI agents fit, and gives reusable templates you can copy into your repository today.&lt;/p&gt;

&lt;h2&gt;
  
  
  SDD Is a Workflow, Not a Document
&lt;/h2&gt;

&lt;p&gt;The most common failure mode in spec-driven development is treating the spec as paperwork. A team writes a long requirements document, stores it in a wiki, and then codes from memory and chat threads. The spec exists, but it does not drive anything. That is documentation theater, and it is worse than no spec because it creates false confidence.&lt;/p&gt;

&lt;p&gt;A working SDD workflow produces a chain of artifacts, each reviewed before the next phase begins. Requirements reduce product ambiguity. Design reduces technical ambiguity. Tasks reduce execution ambiguity. Implementation produces code against a known target. Validation proves the chain held. When any phase reveals a mistake, you fix the artifact and re-run from that point -- not after three thousand lines of drift have landed in main.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Specify] --&amp;gt; B[Plan]
  B --&amp;gt; C[Tasks]
  C --&amp;gt; D[Implement]
  D --&amp;gt; E[Validate]
  E --&amp;gt;|drift found| A
  E --&amp;gt;|ship| F[Done]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow is tool-neutral. You can run it with markdown files in Git, with &lt;a href="https://www.glukhov.org/ai-devtools/ai-coding-assistants/spec-kit-vs-kiro-vs-claude-code/" rel="noopener noreferrer"&gt;GitHub Spec Kit&lt;/a&gt;, with Cursor plans, or with a plain text editor and a disciplined reviewer. What matters is the sequence and the review gates, not the brand on the tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1 -- Specify the Requirements
&lt;/h2&gt;

&lt;p&gt;The specify phase answers what problem you are solving and what done looks like. It deliberately avoids how to build it. The moment your requirements spec says "use Redis sorted sets," you have stopped specifying and started designing in the wrong document. Keep implementation out of requirements. Put it in the plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem statement and users
&lt;/h3&gt;

&lt;p&gt;Start with one paragraph that states the problem in plain language. Name the users affected and the situation that makes the problem painful. A good problem statement lets a reviewer who was not in the planning meeting decide whether a proposed solution actually addresses the pain.&lt;/p&gt;

&lt;p&gt;Example for an API rate-limiting feature:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;API consumers on the free tier can send unlimited requests, which causes cost spikes and noisy-neighbor impact on paid tenants. Platform operators need a enforceable per-key limit without manual intervention.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Goals, non-goals, and acceptance criteria
&lt;/h3&gt;

&lt;p&gt;Goals describe outcomes you will deliver. Non-goals describe tempting adjacent work you will explicitly not do. Together they bound the agent's creativity, which is essential when AI tools otherwise "helpfully" expand scope.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Good example&lt;/th&gt;
&lt;th&gt;Weak example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Goal&lt;/td&gt;
&lt;td&gt;Reject requests over the per-key limit with HTTP 429&lt;/td&gt;
&lt;td&gt;Make the API faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-goal&lt;/td&gt;
&lt;td&gt;Per-tenant billing dashboards&lt;/td&gt;
&lt;td&gt;Improve all API performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Acceptance criterion&lt;/td&gt;
&lt;td&gt;Unauthenticated requests receive 401 before rate check runs&lt;/td&gt;
&lt;td&gt;The endpoint is secure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Acceptance criteria should be precise enough that each one maps to at least one test. "The endpoint is secure" is not an acceptance criterion. "Unauthenticated requests receive HTTP 401" is. If you cannot write a concrete criterion, the requirement is still too vague to implement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open questions
&lt;/h3&gt;

&lt;p&gt;List every decision that is not yet settled. Unclear questions are not a sign of failure. They are the specify phase doing its job. Resolve them before you write the design plan, or you will pay for the ambiguity in implementation rework.&lt;/p&gt;

&lt;p&gt;A minimal requirements template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Problem&lt;/span&gt;
[One paragraph: who hurts, why, and what triggers the pain.]

&lt;span class="gu"&gt;## Users&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [Primary user role]
&lt;span class="p"&gt;-&lt;/span&gt; [Secondary user role]

&lt;span class="gu"&gt;## Goals&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; [Measurable outcome]
&lt;span class="p"&gt;2.&lt;/span&gt; [Measurable outcome]

&lt;span class="gu"&gt;## Non-goals&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [Explicitly out of scope]
&lt;span class="p"&gt;-&lt;/span&gt; [Explicitly out of scope]

&lt;span class="gu"&gt;## Acceptance criteria&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] [Verifiable behavior]
&lt;span class="p"&gt;-&lt;/span&gt; [ ] [Verifiable behavior]

&lt;span class="gu"&gt;## Open questions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] [Question that blocks planning]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Phase 2 -- Plan the Design
&lt;/h2&gt;

&lt;p&gt;The plan phase translates intent into technical decisions. This is where Redis sorted sets belong, along with module boundaries, schema changes, API contracts, migration steps, security constraints, and the test strategy. The plan is derived from the requirements spec plus your project's existing constraints -- stack choices, &lt;a href="https://www.glukhov.org/app-architecture/documentation/decision-records-ai-driven-development/" rel="noopener noreferrer"&gt;decision records&lt;/a&gt;, and conventions stored in files like &lt;code&gt;AGENTS.md&lt;/code&gt; or a project constitution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture and affected modules
&lt;/h3&gt;

&lt;p&gt;Name the modules, services, or packages that will change and summarize the integration pattern. If the feature crosses a service boundary, document the contract on both sides. Agents hallucinate APIs when contracts are implicit. Making them explicit in the plan prevents invented endpoints and wrong response shapes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data model, API contracts, and migrations
&lt;/h3&gt;

&lt;p&gt;Document schema changes, new tables or fields, index requirements, and backward-compatibility rules. For HTTP APIs, write method, path, request shape, response shape, and error codes. For events, write topic names, payload schemas, and delivery semantics. Include migration steps and rollback notes when the data model changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security, observability, and test strategy
&lt;/h3&gt;

&lt;p&gt;Security constraints belong in the plan, not as afterthoughts in code review. Note authentication requirements, authorization rules, input validation boundaries, and data that must not appear in logs. Observability should cover metrics, logs, or traces needed to confirm the feature works in production.&lt;/p&gt;

&lt;p&gt;The test strategy connects back to acceptance criteria. Identify which criteria need unit tests, which need integration tests, and which need manual verification. If you use &lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/unit-testing-in-go/" rel="noopener noreferrer"&gt;unit testing in Go&lt;/a&gt; or &lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/unit-testing-in-python/" rel="noopener noreferrer"&gt;unit testing in Python&lt;/a&gt;, name the packages and test files you expect to add. A plan without a test strategy is a plan that will ship with gaps you discover in production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
  subgraph plan [Design plan contents]
    R[Requirements spec]
    C[Project constitution / ADRs]
    R --&amp;gt; D[Architecture decisions]
    C --&amp;gt; D
    D --&amp;gt; M[Data model and migrations]
    D --&amp;gt; A[API contracts]
    D --&amp;gt; S[Security constraints]
    D --&amp;gt; T[Test strategy]
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Phase 3 -- Break Down Implementation Tasks
&lt;/h2&gt;

&lt;p&gt;The task phase decomposes the plan into slices small enough to implement, review, and validate independently. This is what makes agent-assisted development reviewable. Instead of one enormous diff, you get a sequence of focused changes that each map back to a named requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Task sizing and dependencies
&lt;/h3&gt;

&lt;p&gt;A good task touches a bounded set of files, completes in one agent session, and ends with a verification step. Tasks should declare dependencies explicitly. Migration tasks run before code that reads the new schema. Shared library changes run before consumers. Authentication middleware changes run before endpoints that depend on the new behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  T1[Task 1 -- schema migration] --&amp;gt; T2[Task 2 -- repository layer]
  T2 --&amp;gt; T3[Task 3 -- HTTP handler]
  T2 --&amp;gt; T4[Task 4 -- metrics instrumentation]
  T3 --&amp;gt; T5[Task 5 -- integration tests]
  T4 --&amp;gt; T5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Files, validation, and review checkpoints
&lt;/h3&gt;

&lt;p&gt;Each task should list the files likely to change, the acceptance criteria it satisfies, and how to validate completion. Validation might be a test command, a curl example, or a manual check described in copy-pasteable steps. Every task ends at a human review checkpoint. The reviewer confirms the diff matches the task description before the next task starts.&lt;/p&gt;

&lt;p&gt;A minimal task entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### Task 3 -- Add rate-limit middleware&lt;/span&gt;

&lt;span class="gs"&gt;**Depends on:**&lt;/span&gt; Task 1 (schema), Task 2 (repository)
&lt;span class="gs"&gt;**Files:**&lt;/span&gt; middleware/ratelimit.go, middleware/ratelimit_test.go, server.go
&lt;span class="gs"&gt;**Satisfies:**&lt;/span&gt; AC-2 (429 over limit), AC-3 (limit headers in response)
&lt;span class="gs"&gt;**Validate:**&lt;/span&gt; &lt;span class="sb"&gt;`go test ./middleware/...`&lt;/span&gt; passes; curl over limit returns 429 with Retry-After
&lt;span class="gs"&gt;**Review checkpoint:**&lt;/span&gt; Confirm middleware runs after auth, before handler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch for generated task explosions. AI agents can produce fifty-task plans in seconds. Most of those tasks will be redundant or too granular to review efficiently. A useful task list for a medium feature often has five to fifteen items, not fifty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4 -- Implement One Task at a Time
&lt;/h2&gt;

&lt;p&gt;Implementation is deliberately narrow. Pick one task, give the agent only the context it needs for that task, and stop when validation passes. Context resets between tasks are a feature, not a bug. They prevent earlier assumptions from polluting later work and keep diffs reviewable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apply constraints from the spec stack
&lt;/h3&gt;

&lt;p&gt;The implementing agent should read the requirements spec, the design plan, the current task description, and project-level constraints. Constraints are the highest-ROI section most teams skip. They tell the agent what not to do -- do not refactor unrelated modules, do not change public API signatures outside this feature, do not introduce new dependencies without updating the plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update the plan when reality differs
&lt;/h3&gt;

&lt;p&gt;Implementation will surface surprises. A library does not support the assumed behavior. A migration takes longer than expected. An edge case was missing from acceptance criteria. When that happens, update the spec before continuing. Fix the requirements or plan, get a quick review, then resume implementation against the corrected artifact. Code that diverges silently from the spec is how drift becomes permanent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
  participant H as Human reviewer
  participant A as AI agent
  participant S as Spec artifacts
  H-&amp;gt;&amp;gt;S: Approve task N
  A-&amp;gt;&amp;gt;S: Read task + plan + constraints
  A-&amp;gt;&amp;gt;A: Implement task N
  A-&amp;gt;&amp;gt;A: Run task validation
  A-&amp;gt;&amp;gt;H: Submit diff for review
  H-&amp;gt;&amp;gt;H: Review diff against task
  alt drift or surprise
    H-&amp;gt;&amp;gt;S: Update spec/plan
    H-&amp;gt;&amp;gt;A: Re-run with corrected context
  else approved
    H-&amp;gt;&amp;gt;S: Mark task N complete
    H-&amp;gt;&amp;gt;A: Proceed to task N+1
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Phase 5 -- Validate Against the Spec
&lt;/h2&gt;

&lt;p&gt;Validation is where SDD earns its keep. Without it, the spec is a planning exercise. With it, the spec is a contract you can check against the shipped code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automated checks
&lt;/h3&gt;

&lt;p&gt;Run the full test suite, lint, and type checks on CI. Wire these into your pipeline using patterns from the &lt;a href="https://www.glukhov.org/developer-tools/ci-cd/github-actions-cheatsheet/" rel="noopener noreferrer"&gt;GitHub Actions cheatsheet&lt;/a&gt; if you need a practical starting point. Automated checks catch regressions. They do not catch wrong features built correctly, which is why acceptance criteria review still matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Acceptance criteria and manual review
&lt;/h3&gt;

&lt;p&gt;Walk through each acceptance criterion from the requirements spec. Mark each as satisfied, failed, or deferred with justification. Manual review catches UX issues, security gaps, and wrong behavior that tests missed because the tests were written to match a flawed spec.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spec-to-code diff
&lt;/h3&gt;

&lt;p&gt;The final validation step compares the implementation against the design plan. Did the files that changed match the files the plan predicted? Did architectural decisions in the code match the recorded decisions? Unexpected files in the diff are a signal -- either the plan was incomplete or the agent wandered. Both deserve attention before merge.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Validation layer&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit and integration tests&lt;/td&gt;
&lt;td&gt;Regressions and incorrect logic within scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lint and type checks&lt;/td&gt;
&lt;td&gt;Style issues and type errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Acceptance criteria walkthrough&lt;/td&gt;
&lt;td&gt;Wrong behavior built to spec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spec-to-code diff&lt;/td&gt;
&lt;td&gt;Architectural drift and scope creep&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where AI Agents Fit in the Workflow
&lt;/h2&gt;

&lt;p&gt;AI agents are accelerators on each phase, not replacements for review. The productive pattern is draft, review, refine, then proceed. Ask an agent to draft the requirements spec from a problem description, then edit intent until goals, non-goals, and acceptance criteria are right. Ask an agent to draft the design plan from the approved requirements, then review architecture decisions before any code exists. Ask an agent to implement one task slice at a time, with you approving each diff before the next task starts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph human [Human owns]
    H1[Intent and priorities]
    H2[Architecture approval]
    H3[Diff review at checkpoints]
    H4[Final acceptance]
  end
  subgraph agent [Agent accelerates]
    A1[Draft requirements]
    A2[Draft design plan]
    A3[Generate task list]
    A4[Implement task slices]
    A5[Draft tests]
  end
  H1 --&amp;gt; A1 --&amp;gt; H1
  A1 --&amp;gt; A2 --&amp;gt; H2
  H2 --&amp;gt; A3 --&amp;gt; A4 --&amp;gt; H3
  H3 --&amp;gt; A4
  A4 --&amp;gt; A5 --&amp;gt; H4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents are especially useful at producing first drafts and boilerplate tests. Humans are especially useful at catching wrong goals, unsafe architecture, and subtle scope creep. The workflow fails when either side is skipped -- when agents implement without specs, or when humans write specs without ever validating them against code.&lt;/p&gt;

&lt;p&gt;This workflow article stays tool-neutral on purpose. Tool-specific execution guides -- editor setup, slash commands, agent configuration -- belong under the &lt;a href="https://www.glukhov.org/ai-devtools/" rel="noopener noreferrer"&gt;AI Developer Tools&lt;/a&gt; cluster. The process pillar lives here under documentation practices because the artifacts matter more than the vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes That Kill Spec-Driven Development
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Huge specs before any validation.&lt;/strong&gt; A thirty-page requirements document written before a prototype or spike is waterfall paperwork, not SDD. Write the minimum spec that removes ambiguity for the next phase, then validate assumptions early. Not every feature needs the full five-phase loop -- &lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/spec-driven-development-vs-vibe-coding/" rel="noopener noreferrer"&gt;Spec-Driven Development vs Vibe Coding&lt;/a&gt; explains when lighter structure is enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vague acceptance criteria.&lt;/strong&gt; Adjectives like "fast," "clean," and "user-friendly" are not acceptance criteria. Replace them with measurable behavior. If you cannot test it, you cannot implement it reliably -- especially with an AI agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing non-goals.&lt;/strong&gt; Without non-goals, agents expand scope by default. They add caching layers, refactor neighboring modules, and introduce dependencies you did not ask for. Non-goals are how you say no in advance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No test plan in the design phase.&lt;/strong&gt; Tests written only after implementation tend to confirm what was built, not what was intended. The plan should name which acceptance criteria map to which test types before the first production file changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skipping review at phase boundaries.&lt;/strong&gt; The spec reviewed before the plan. The plan reviewed before tasks. Tasks reviewed before implementation. Each gate is cheap. Fixing drift after a large merge is expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Letting generated tasks explode.&lt;/strong&gt; Treat a fifty-item AI-generated task list as a first draft, not a schedule. Merge redundant items, split oversized ones, and delete tasks that do not map to a requirement.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;SDD works when each phase reduces ambiguity. It fails when it creates paperwork.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Reusable Templates
&lt;/h2&gt;

&lt;p&gt;Copy these into your repository and adapt them. Store specs alongside the feature branch, review them in pull requests, and keep them in version control so agents and humans read the same source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requirements template
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Feature -- [name]&lt;/span&gt;

&lt;span class="gu"&gt;## Problem&lt;/span&gt;
&lt;span class="gu"&gt;## Users&lt;/span&gt;
&lt;span class="gu"&gt;## Goals&lt;/span&gt;
&lt;span class="gu"&gt;## Non-goals&lt;/span&gt;
&lt;span class="gu"&gt;## Acceptance criteria&lt;/span&gt;
&lt;span class="gu"&gt;## Open questions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Design template
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Design -- [feature name]&lt;/span&gt;

&lt;span class="gu"&gt;## Summary&lt;/span&gt;
&lt;span class="gu"&gt;## Affected modules&lt;/span&gt;
&lt;span class="gu"&gt;## Data model changes&lt;/span&gt;
&lt;span class="gu"&gt;## API contracts&lt;/span&gt;
&lt;span class="gu"&gt;## Migrations&lt;/span&gt;
&lt;span class="gu"&gt;## Security&lt;/span&gt;
&lt;span class="gu"&gt;## Observability&lt;/span&gt;
&lt;span class="gu"&gt;## Test strategy&lt;/span&gt;
&lt;span class="gu"&gt;## Risks and mitigations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Task list template
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Tasks -- [feature name]&lt;/span&gt;

&lt;span class="gu"&gt;## Task 1 -- [title]&lt;/span&gt;
Depends on:
Files:
Satisfies:
Validate:
Review checkpoint:

&lt;span class="gu"&gt;## Task 2 -- [title]&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Validation checklist
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Validation -- [feature name]&lt;/span&gt;

&lt;span class="gu"&gt;## Automated&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] All tests pass
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Lint clean
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Type check clean

&lt;span class="gu"&gt;## Acceptance criteria&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] AC-1 --
&lt;span class="p"&gt;-&lt;/span&gt; [ ] AC-2 --

&lt;span class="gu"&gt;## Spec-to-code&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Changed files match plan
&lt;span class="p"&gt;-&lt;/span&gt; [ ] No undocumented architectural changes
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Spec updated if implementation differed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Spec-driven development is not about writing more documents. It is about moving through specify, plan, task, implement, and validate with a review gate at each step. Each phase should leave the next actor -- human or agent -- with less guesswork than the phase before.&lt;/p&gt;

&lt;p&gt;Start small. Run the full workflow on one medium-sized feature. Keep artifacts in markdown in the repository. Update the spec when reality diverges. Validate before merge. When the chain works, you get less drift, smaller reviewable diffs, and a durable record of intent that survives session resets and team handoffs.&lt;/p&gt;

&lt;p&gt;When the chain becomes paperwork, cut scope -- not review. A two-page spec that was validated beats a thirty-page spec that nobody read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.github.io/spec-kit/" rel="noopener noreferrer"&gt;GitHub Spec Kit documentation&lt;/a&gt; -- open-source toolkit that implements a similar specify-plan-tasks-implement loop&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html" rel="noopener noreferrer"&gt;Martin Fowler on Spec-Driven Development tools&lt;/a&gt; -- analysis of Kiro, Spec Kit, and Tessl&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>documentation</category>
      <category>aicoding</category>
      <category>architecture</category>
      <category>workflow</category>
    </item>
    <item>
      <title>GitHub Spec Kit vs Kiro vs Claude Code SDD Workflows</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sun, 12 Jul 2026 08:29:43 +0000</pubDate>
      <link>https://dev.to/rosgluk/github-spec-kit-vs-kiro-vs-claude-code-sdd-workflows-1fi1</link>
      <guid>https://dev.to/rosgluk/github-spec-kit-vs-kiro-vs-claude-code-sdd-workflows-1fi1</guid>
      <description>&lt;p&gt;Developers comparing Spec-Driven Development setups in 2026 are usually not asking which model is smartest. They are asking which workflow will keep an AI agent aligned without burying them in ceremony.&lt;/p&gt;

&lt;p&gt;GitHub Spec Kit, AWS Kiro, and Claude Code custom workflows all implement the same broad idea -- requirements, design, tasks, implementation, validation -- but they trade off portability, integration depth, and how much process they enforce.&lt;/p&gt;

&lt;p&gt;If you need the concepts first, read &lt;a href="https://www.glukhov.org/app-architecture/documentation/what-is-spec-driven-development/" rel="noopener noreferrer"&gt;What Is Spec-Driven Development?&lt;/a&gt; and the tool-neutral &lt;a href="https://www.glukhov.org/app-architecture/documentation/spec-driven-development-workflow/" rel="noopener noreferrer"&gt;Spec-Driven Development Workflow&lt;/a&gt; guide in the &lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;App Architecture&lt;/a&gt; documentation cluster. This comparison sits in the &lt;a href="https://www.glukhov.org/ai-devtools/" rel="noopener noreferrer"&gt;AI Developer Tools&lt;/a&gt; hub alongside assistant reviews and workflow guides. It covers the three setups developers argue about most on Hacker News and Reddit-style forums, plus the lighter and heavier alternatives orbiting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  SDD Is Becoming a Tool Category
&lt;/h2&gt;

&lt;p&gt;Spec-Driven Development stopped being a paper exercise sometime in late 2025. Every major AI coding vendor now ships some version of specify-plan-implement, and a growing list of standalone tools competes on how much structure they add around that loop.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool / approach&lt;/th&gt;
&lt;th&gt;Maintainer&lt;/th&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;Typical strength&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Spec Kit&lt;/td&gt;
&lt;td&gt;GitHub (open source)&lt;/td&gt;
&lt;td&gt;CLI scaffolding, multi-file artifacts, 30+ agents&lt;/td&gt;
&lt;td&gt;Portability across editors and agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kiro&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;Spec-native IDE (VS Code fork) plus CLI&lt;/td&gt;
&lt;td&gt;Guided workflow inside one environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code skills/commands&lt;/td&gt;
&lt;td&gt;Anthropic ecosystem&lt;/td&gt;
&lt;td&gt;Lightweight repo-local workflows&lt;/td&gt;
&lt;td&gt;Fast to customize, easy to hack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenSpec&lt;/td&gt;
&lt;td&gt;Fission AI (community)&lt;/td&gt;
&lt;td&gt;Change-centric, fewer artifacts&lt;/td&gt;
&lt;td&gt;Brownfield iteration with lower overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BMAD-METHOD&lt;/td&gt;
&lt;td&gt;Community&lt;/td&gt;
&lt;td&gt;Multi-agent, role-based ceremony&lt;/td&gt;
&lt;td&gt;Large features with explicit role simulation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tessl&lt;/td&gt;
&lt;td&gt;Tessl (commercial, beta)&lt;/td&gt;
&lt;td&gt;Spec-as-source code generation&lt;/td&gt;
&lt;td&gt;Strong traceability, higher lock-in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The comparison that matters is not "which tool wins." It is &lt;strong&gt;process depth versus portability&lt;/strong&gt;. Kiro is integrated. Spec Kit is portable. Claude Code workflows are hackable. Bad specs make every agent worse regardless of which wrapper you choose. Good specs travel across tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph portable [Portable]
    SK[Spec Kit]
    CC[Claude Code skills]
    OS[OpenSpec]
  end
  subgraph integrated [Integrated]
    KI[Kiro IDE]
    TE[Tessl]
  end
  portable --&amp;gt; M[Markdown specs in Git]
  integrated --&amp;gt; E[Editor-native loop]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to Compare SDD Setups
&lt;/h2&gt;

&lt;p&gt;Before picking a tool, name what you are optimizing for. The same feature can feel effortless in one setup and bureaucratic in another depending on team size, codebase age, and how much review you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portability&lt;/strong&gt; -- Can the specs live as plain markdown in your repository and work with the agent you prefer next quarter? Or are they tied to one IDE, one cloud, or one proprietary format?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup friction&lt;/strong&gt; -- How long from "I want to try SDD" to a working specify-plan-tasks loop? CLI scaffolding, IDE install, or rolling your own slash commands all have different activation energy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec quality&lt;/strong&gt; -- Does the tool help you write precise requirements and acceptance criteria, or does it mostly generate long documents? Structure is useful. Volume is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task execution&lt;/strong&gt; -- How does the tool break work into reviewable slices? Can tasks run in parallel? Does it resist fifty-item task explosions?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review checkpoints&lt;/strong&gt; -- Are there natural human gates between specify, plan, tasks, and implement? SDD without review is just slower vibe coding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository grounding&lt;/strong&gt; -- Does the workflow read project conventions, &lt;a href="https://www.glukhov.org/app-architecture/documentation/decision-records-ai-driven-development/" rel="noopener noreferrer"&gt;decision records&lt;/a&gt;, ADRs, &lt;code&gt;AGENTS.md&lt;/code&gt;, and existing code before planning? Agents without grounding reinvent architecture because they never see the reviewed intent behind prior choices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team collaboration&lt;/strong&gt; -- Can multiple people review the same spec artifacts in pull requests? Can you mix agents without rewriting the process?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock-in&lt;/strong&gt; -- What do you lose if you switch editors, models, or cloud vendors in six months?&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Spec Kit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.github.io/spec-kit/" rel="noopener noreferrer"&gt;GitHub Spec Kit&lt;/a&gt; is an open-source CLI toolkit that scaffolds a spec-driven loop into your repository and hands execution to whichever coding agent you already use. The &lt;code&gt;specify&lt;/code&gt; CLI drops templates, slash commands, and a conventional folder layout. Typical commands follow a constitution-specify-clarify-plan-tasks-implement sequence, with an explicit clarify step to resolve ambiguity before architecture work begins.&lt;/p&gt;

&lt;p&gt;Spec Kit's defining advantage is &lt;strong&gt;agent independence&lt;/strong&gt;. The official docs position it as tooling that works with Claude Code, &lt;a href="https://www.glukhov.org/ai-devtools/github-copilot-cheatsheet/" rel="noopener noreferrer"&gt;GitHub Copilot&lt;/a&gt;, Cursor, Gemini CLI, Codex, and dozens of other agents. You write specs once in markdown, commit them like code, and swap the executor without rewriting the process. That makes Spec Kit the default recommendation for teams that want SDD without betting on a single vendor.&lt;/p&gt;

&lt;p&gt;The tradeoffs are real. Spec Kit can produce a large artifact tree -- constitution, spec, plan, tasks, contracts -- which pays off on multi-session features but feels heavy for a small CLI tweak. Hacker News threads regularly compare that overhead to waterfall ceremony. Spec Kit is also weaker if you want a fully integrated IDE where specs, tasks, and implementation live in one guided surface. It layers process on top of your existing editor rather than replacing it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free, MIT licensed, repo-portable&lt;/td&gt;
&lt;td&gt;No built-in IDE integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works with 30+ coding agents&lt;/td&gt;
&lt;td&gt;Can generate verbose artifact sets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit clarify and review phases&lt;/td&gt;
&lt;td&gt;You assemble editor + agent + CLI yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Specs are plain markdown in Git&lt;/td&gt;
&lt;td&gt;No automatic bidirectional spec sync&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Spec Kit fits teams that already have a preferred &lt;a href="https://www.glukhov.org/ai-devtools/ai-coding-assistants/" rel="noopener noreferrer"&gt;AI coding assistant&lt;/a&gt; and want a standardized SDD scaffold on top. It is especially strong for greenfield features, multi-agent shops, and anyone who refuses editor lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Kiro
&lt;/h2&gt;

&lt;p&gt;Kiro is AWS's spec-driven IDE, built on a VS Code / Code OSS fork. Where Spec Kit brings SDD to your existing stack, Kiro assumes SDD deserves a purpose-built environment. A prompt generates structured artifacts -- typically &lt;code&gt;requirements.md&lt;/code&gt; in EARS-style notation, &lt;code&gt;design.md&lt;/code&gt;, and a dependency-sequenced &lt;code&gt;tasks.md&lt;/code&gt; -- before agents write production code.&lt;/p&gt;

&lt;p&gt;The guided experience is Kiro's main selling point. Requirements, design, and tasks are first-class UI objects beside your code, not files you manage through a separate CLI. Kiro also ships &lt;strong&gt;Agent Hooks&lt;/strong&gt;, event-driven automations that can update tests, docs, or related artifacts when implementation changes. That bidirectional loop is something Spec Kit does not provide out of the box -- Spec Kit specs stay static until a human updates them.&lt;/p&gt;

&lt;p&gt;The costs are integration depth traded for portability. Kiro runs inside its editor, uses AWS Bedrock-backed models, and bills through a credit-based pricing model with tiered plans. Enterprise teams already on AWS infrastructure often find that acceptable. Solo developers and multi-editor teams may not. Kiro also has rough edges typical of a newer IDE -- extension compatibility, workflow surprises, and the usual "do I really need another editor?" question.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tight requirements-design-tasks loop in one IDE&lt;/td&gt;
&lt;td&gt;Editor and cloud ecosystem lock-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EARS-style requirements rigor&lt;/td&gt;
&lt;td&gt;Credit-metered pricing surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent Hooks for spec-code sync&lt;/td&gt;
&lt;td&gt;Weaker appeal outside AWS-native shops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strong traceability from requirement to task&lt;/td&gt;
&lt;td&gt;Harder to mix arbitrary external agents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Kiro fits developers who want the &lt;strong&gt;most guided SDD experience&lt;/strong&gt; and are comfortable adopting a spec-native IDE. It is a strong option for enterprise teams, AWS-heavy environments, and anyone migrating from Amazon Q Developer who wants spec discipline without assembling the toolchain manually. If you live in standard &lt;a href="https://www.glukhov.org/developer-tools/editors-ides/vscode-cheatsheet/" rel="noopener noreferrer"&gt;VS Code&lt;/a&gt; today and love your current setup, Kiro asks for a bigger switch than Spec Kit does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code Custom Commands and Skills
&lt;/h2&gt;

&lt;p&gt;Claude Code does not ship a single official SDD product the way Spec Kit or Kiro do. If you are new to the tool itself, start with the &lt;a href="https://www.glukhov.org/ai-devtools/claude-code/" rel="noopener noreferrer"&gt;Claude Code install and config guide&lt;/a&gt; for setup, permissions, and local backends. The SDD pattern itself lives in &lt;strong&gt;custom commands&lt;/strong&gt;, &lt;strong&gt;skills&lt;/strong&gt;, and repo-local markdown templates that developers maintain. Anthropic folded older &lt;code&gt;.claude/commands/*.md&lt;/code&gt; files into the Skills mechanism, so the durable pattern is a &lt;code&gt;SKILL.md&lt;/code&gt; (or equivalent) that defines your specify-plan-implement checklist, loaded on demand.&lt;/p&gt;

&lt;p&gt;This approach is the lightest and most hackable. You can port a Kiro-style three-file layout, mirror Spec Kit phases with slash commands, or invent a minimal workflow that fits one repository. Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt; for always-on project context and pulls skills when the task matches. That progressive disclosure keeps sessions focused without loading a full constitution on every prompt.&lt;/p&gt;

&lt;p&gt;The downside is discipline. Nothing forces you through clarify or review gates unless you build those gates yourself. Reddit and Hacker News threads about "spec-driven development inside Claude Code" are full of developers who copied someone else's skill, ran it once, and went back to unstructured prompting when the skill felt slow. Claude Code SDD works when you treat skills like code -- versioned, reviewed, and maintained -- not like a one-time prompt download.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fast to customize per repo&lt;/td&gt;
&lt;td&gt;No enforced workflow without your own rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portable markdown specs in Git&lt;/td&gt;
&lt;td&gt;Quality depends entirely on author discipline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skills reusable across compatible clients&lt;/td&gt;
&lt;td&gt;No built-in multi-agent orchestration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lowest ceremony for solo developers&lt;/td&gt;
&lt;td&gt;Easy to drift back to vibe coding&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a serious implementation, read &lt;a href="https://www.glukhov.org/ai-devtools/claude-code/claude-skills-for-developers/" rel="noopener noreferrer"&gt;Claude Skills and SKILL.md for Developers&lt;/a&gt; and encode your phases as skills with explicit review checkpoints. Claude Code SDD is the right pick when you already live in Claude Code, want &lt;strong&gt;maximum flexibility&lt;/strong&gt;, and will maintain the workflow yourself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
  participant D as Developer
  participant S as Spec artifacts
  participant A as Coding agent
  Note over D,S: Spec Kit / Kiro / Claude skill
  D-&amp;gt;&amp;gt;S: Specify requirements
  D-&amp;gt;&amp;gt;S: Review and approve plan
  D-&amp;gt;&amp;gt;S: Approve task list
  D-&amp;gt;&amp;gt;A: Implement one task
  A-&amp;gt;&amp;gt;D: Diff for review
  D-&amp;gt;&amp;gt;S: Update spec if drift found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  BMAD, OpenSpec, and Other Workflows
&lt;/h2&gt;

&lt;p&gt;Not every team wants the Spec Kit artifact tree or the Kiro IDE. Two alternatives show up constantly in 2026 comparisons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenSpec&lt;/strong&gt; (Fission AI) takes a change-centric approach with fewer generated files than Spec Kit. Community benchmarks report materially lower token usage for comparable tasks, at the cost of less upfront structure. OpenSpec tends to win when you are modifying an existing codebase and want reviewable specs without an 800-line planning phase. It competes with Spec Kit on portability more than with Kiro on IDE integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BMAD-METHOD&lt;/strong&gt; (community) pushes in the opposite direction -- multi-agent, role-based workflows that simulate product owner, architect, developer, and reviewer personas. BMAD can be powerful on large greenfield efforts where explicit role separation helps. It is also heavy. Teams frequently report that the ceremony only pays off when coordination pain is already acute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tessl&lt;/strong&gt; treats the spec as the literal source of generated code, marking output as derived and discouraging hand-edits. That is the strongest "spec-as-source" stance among mainstream tools, but Tessl remains beta and carries the highest product lock-in of the group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec Kitty&lt;/strong&gt; and other community scaffolds sit between OpenSpec and Spec Kit on weight. They are worth watching if you want templates without adopting the full GitHub toolchain.&lt;/p&gt;

&lt;p&gt;The pattern across all of them is the same. More process helps when ambiguity is expensive. More process hurts when feedback speed matters more than alignment. Match tool weight to task size, not to hype.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which SDD Setup Should You Use?
&lt;/h2&gt;

&lt;p&gt;There is no universal winner. The right setup depends on who you are, what you are building, and how much structure you will actually maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solo developer, existing codebase, small features.&lt;/strong&gt; Start with Claude Code skills or OpenSpec. Write a short requirements block, a minimal task list, and one review checkpoint. Do not install a full Spec Kit tree for a fifty-line change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solo developer, greenfield feature, multiple sessions.&lt;/strong&gt; Spec Kit or a well-maintained Claude Code SDD skill. You need durable artifacts more than IDE hand-holding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small team, mixed editors.&lt;/strong&gt; Spec Kit. Plain markdown specs in Git, reviewed in pull requests, executed by whichever agent each developer prefers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise team, AWS-native, compliance pressure.&lt;/strong&gt; Kiro. Guided artifacts, requirement traceability, and hooks that keep docs and tests closer to implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regulated environment.&lt;/strong&gt; Kiro or Spec Kit plus your own validation checklist -- not Claude Code skills alone unless you encode compliance gates explicitly. Tooling does not replace audit trails. It only makes them easier to produce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Existing codebase, brownfield change.&lt;/strong&gt; OpenSpec or a lightweight Claude Code workflow. Full Spec Kit ceremony on every bugfix will feel like waterfall. Reserve heavier structure for cross-cutting features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Greenfield product, many agents.&lt;/strong&gt; Spec Kit. Portability matters more than IDE polish when Copilot, Claude Code, and Cursor may all touch the same repo.&lt;/p&gt;

&lt;p&gt;Teams experimenting with multi-agent orchestration should also look at &lt;a href="https://www.glukhov.org/ai-devtools/opencode/oh-my-opencode-agents/" rel="noopener noreferrer"&gt;Oh My OpenCode Agents&lt;/a&gt; for patterns on splitting roles across agents -- complementary to SDD artifacts, not a replacement for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Decision Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If you want...&lt;/th&gt;
&lt;th&gt;Start here&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Least lock-in&lt;/td&gt;
&lt;td&gt;Spec Kit or plain markdown + Claude skills&lt;/td&gt;
&lt;td&gt;Specs in Git, swap agents freely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best guided IDE experience&lt;/td&gt;
&lt;td&gt;Kiro&lt;/td&gt;
&lt;td&gt;Requirements, design, tasks built into the editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code only, minimal setup&lt;/td&gt;
&lt;td&gt;Custom SDD skill in &lt;code&gt;.claude/skills/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fast, hackable, repo-local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team review in pull requests&lt;/td&gt;
&lt;td&gt;Spec Kit or OpenSpec&lt;/td&gt;
&lt;td&gt;Markdown artifacts diff cleanly in PRs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security / compliance traceability&lt;/td&gt;
&lt;td&gt;Kiro + explicit validation checklist&lt;/td&gt;
&lt;td&gt;Requirement-to-task mapping plus hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lowest token overhead&lt;/td&gt;
&lt;td&gt;OpenSpec or lightweight Claude workflow&lt;/td&gt;
&lt;td&gt;Fewer generated artifacts per change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum process for large builds&lt;/td&gt;
&lt;td&gt;BMAD-METHOD&lt;/td&gt;
&lt;td&gt;Role-based multi-agent ceremony&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spec literally drives generated code&lt;/td&gt;
&lt;td&gt;Tessl (evaluate beta risk)&lt;/td&gt;
&lt;td&gt;Strongest spec-as-source model&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  Q1{Need a new IDE?}
  Q1 --&amp;gt;|Yes, AWS OK| K[Kiro]
  Q1 --&amp;gt;|No| Q2{Team uses many agents?}
  Q2 --&amp;gt;|Yes| SK[Spec Kit]
  Q2 --&amp;gt;|No| Q3{Already on Claude Code?}
  Q3 --&amp;gt;|Yes| CC[Claude Code SDD skill]
  Q3 --&amp;gt;|No| SK
  Q4{Brownfield small change?}
  Q4 --&amp;gt;|Yes| OS[OpenSpec or minimal spec]
  Q4 --&amp;gt;|No| SK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Actually Determines Success
&lt;/h2&gt;

&lt;p&gt;Tool choice matters less than artifact quality. A Kiro requirements file with vague acceptance criteria will produce the same drift as a sloppy Claude Code prompt. A Spec Kit plan that lists fifty redundant tasks will feel like waterfall regardless of which agent implements it.&lt;/p&gt;

&lt;p&gt;The practices that travel across every setup are boring and effective. Keep specs small enough to review in one sitting. Write non-goals explicitly. Break tasks into diffs a human can read. Validate against acceptance criteria before merge. Update the spec when implementation discovers a better path.&lt;/p&gt;

&lt;p&gt;If you are still choosing between SDD and unstructured prompting for a given feature, read &lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/spec-driven-development-vs-vibe-coding/" rel="noopener noreferrer"&gt;Spec-Driven Development vs Vibe Coding&lt;/a&gt;. The tool comparison in this article only matters once you have decided the feature deserves a spec at all.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Bad specs make every agent worse. Good specs travel across tools.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;GitHub Spec Kit, Kiro, and Claude Code workflows are three answers to the same question -- how do you keep AI agents aligned across sessions -- with different bets on portability versus integration. Spec Kit optimizes for agent-agnostic markdown in your repository. Kiro optimizes for a guided spec-native IDE with AWS-backed agents. Claude Code skills optimize for hackable, lightweight workflows that succeed only when you maintain them.&lt;/p&gt;

&lt;p&gt;Pick the shallowest setup that still removes ambiguity for the feature at hand. Add structure when coordination pain appears, not when a blog post tells you to. The developers who get value from SDD in 2026 are not the ones with the most elaborate toolchain. They are the ones who write specs worth implementing -- then let whichever tool they chose execute against them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.github.io/spec-kit/" rel="noopener noreferrer"&gt;GitHub Spec Kit documentation&lt;/a&gt; -- official Spec Kit workflow reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html" rel="noopener noreferrer"&gt;Martin Fowler on SDD tools&lt;/a&gt; -- analysis of Kiro, Spec Kit, and Tessl&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>specdrivendevelopment</category>
      <category>aicoding</category>
      <category>githubspeckit</category>
      <category>kiro</category>
    </item>
    <item>
      <title>A2A and MCP Agent Security: Identity, Delegation, and Audit Trails</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sat, 11 Jul 2026 06:46:43 +0000</pubDate>
      <link>https://dev.to/rosgluk/a2a-and-mcp-agent-security-identity-delegation-and-audit-trails-k3n</link>
      <guid>https://dev.to/rosgluk/a2a-and-mcp-agent-security-identity-delegation-and-audit-trails-k3n</guid>
      <description>&lt;p&gt;Prompt injection gets most of the security attention in LLM systems, and it deserves attention, but it is not the whole problem once agents start calling tools and delegating work to other agents.&lt;/p&gt;

&lt;p&gt;MCP gives an agent structured access to files, APIs, databases, and ticketing systems. A2A lets one agent send tasks, messages, and artifacts to another agent that may belong to a different team, vendor, or runtime. Those protocols are useful precisely because they cross trust boundaries, which means identity, authorization, delegation limits, and audit trails become first-class architecture rather than optional hardening.&lt;/p&gt;

&lt;p&gt;This article is the canonical guide for &lt;strong&gt;agent protocol security&lt;/strong&gt; in the &lt;a href="https://www.glukhov.org/llm-architecture/" rel="noopener noreferrer"&gt;LLM Architecture&lt;/a&gt; cluster. It covers threat models, identity, gateways, registries, delegation, and production checklists. For input validation, output filtering, and prompt safety patterns, see &lt;a href="https://www.glukhov.org/llm-architecture/guardrails/llm-guardrails-in-practice/" rel="noopener noreferrer"&gt;LLM Guardrails in Practice&lt;/a&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails vs Protocol Security vs Runtime Policy
&lt;/h2&gt;

&lt;p&gt;These three layers solve different problems and fail in different ways when conflated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM guardrails&lt;/strong&gt; operate on model input and output: blocking injection patterns, filtering harmful content, validating JSON shape, and enforcing tone or compliance rules on generated text. They protect the conversation layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protocol security&lt;/strong&gt; operates on agent boundaries: who may call which MCP tool, which agent may delegate to which peer, what OAuth scopes attach to a task, and whether a downstream agent may act on a user's behalf. It protects the action layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime policy&lt;/strong&gt; sits between them: a policy engine that evaluates requests against rules regardless of whether the trigger was natural language or a structured protocol call. It can require human approval before a tool executes, block egress to unknown domains, or deny delegation when scope exceeds the originating user.&lt;/p&gt;

&lt;p&gt;My opinion is blunt: guardrails without protocol security produce polite chatbots that still exfiltrate data through a tool call. Protocol security without guardrails produces well-authenticated agents that still follow malicious instructions embedded in an artifact. You need both, plus runtime policy for high-risk actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threat Model for A2A and MCP Agent Systems
&lt;/h2&gt;

&lt;p&gt;Start with assets and adversaries, not with a shopping list of controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assets worth protecting:&lt;/strong&gt; user data in prompts and artifacts, credentials for MCP servers, production systems reachable through tools, agent reputation, billing accounts tied to token usage, and audit integrity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Realistic adversaries:&lt;/strong&gt; external users abusing public agent endpoints, compromised MCP servers returning poisoned tool results, malicious agents misrepresenting skills in Agent Cards, insiders over-delegating authority, and supply-chain tampering with tool metadata that manipulates model behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Malicious or compromised tools (MCP)
&lt;/h3&gt;

&lt;p&gt;An MCP server is code plus data exposed to the model. A hostile server can return misleading tool descriptions, exfiltrate arguments passed by the model, or perform actions beyond what the user intended when the host executes tool calls without scoped credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  Malicious or impersonated agents (A2A)
&lt;/h3&gt;

&lt;p&gt;An agent that accepts tasks may be evil, compromised, or simply over-permissioned. Agent Cards describe capabilities; they do not prove identity unless you verify signatures, TLS, and issuer trust.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confused deputy
&lt;/h3&gt;

&lt;p&gt;Agent B holds permission to access a finance API. Agent A, with lower privilege, asks B to "summarize this invoice" while smuggling a transfer instruction in an artifact. B executes using its own credentials unless delegation scope is enforced end to end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Over-broad permissions and hidden delegation chains
&lt;/h3&gt;

&lt;p&gt;User approves one step. The orchestrator silently chains three A2A hops and five MCP calls. The user never sees the full graph, but the organization is still accountable for the outcome.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt injection through artifacts and cross-agent messages
&lt;/h3&gt;

&lt;p&gt;Injection is not only a user-message problem. A PDF artifact, a web page fetched by a tool, or a message from Agent C can carry instructions aimed at Agent D's model. Treat &lt;strong&gt;all&lt;/strong&gt; protocol-carried content as untrusted input at the model boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Poisoned or misleading Agent Cards
&lt;/h3&gt;

&lt;p&gt;Descriptions and skill names are prompt surface area. A card that advertises &lt;code&gt;safe_read_only_analysis&lt;/code&gt; while accepting write-capable backends is a social-engineering layer, not a technical guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity Model for Multi-Agent Systems
&lt;/h2&gt;

&lt;p&gt;Protocol security begins with clear identity types and what each one is allowed to prove.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Identity type&lt;/th&gt;
&lt;th&gt;What it represents&lt;/th&gt;
&lt;th&gt;Typical proof&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Human user&lt;/td&gt;
&lt;td&gt;End user or operator who initiated work&lt;/td&gt;
&lt;td&gt;OIDC session, SSO token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent service&lt;/td&gt;
&lt;td&gt;Deployed agent runtime (orchestrator, specialist)&lt;/td&gt;
&lt;td&gt;OAuth client credentials, mTLS cert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP server&lt;/td&gt;
&lt;td&gt;Tool provider process&lt;/td&gt;
&lt;td&gt;API key, mTLS, scoped service account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task / session&lt;/td&gt;
&lt;td&gt;Unit of work spanning hops&lt;/td&gt;
&lt;td&gt;task ID, trace ID, delegated scope token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A2A's Agent Card advertises &lt;strong&gt;supported authentication schemes&lt;/strong&gt; (OAuth 2.0, API keys, mTLS, and similar patterns aligned with OpenAPI practice) and &lt;strong&gt;skills&lt;/strong&gt; with optional security requirements. The card is discovery metadata, not a trust anchor. Clients obtain credentials out of band and send them in standard HTTP headers on every request; servers must validate on every call and return 401 or 403 when auth or scope fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Internal vs external views of the same agent
&lt;/h3&gt;

&lt;p&gt;Production agents often publish a &lt;strong&gt;public&lt;/strong&gt; Agent Card with a limited skill list and a richer &lt;strong&gt;authenticated&lt;/strong&gt; card for internal callers. The A2A specification allows extended cards for authenticated clients. Use that split deliberately: partners should not see internal skills, and internal orchestrators should not rely on public discovery alone for authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication and Authorization for MCP and A2A
&lt;/h2&gt;

&lt;p&gt;Authentication answers &lt;strong&gt;who is calling&lt;/strong&gt;. Authorization answers &lt;strong&gt;what they may do&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP tool access
&lt;/h3&gt;

&lt;p&gt;For each MCP connection, define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which agent host may connect&lt;/li&gt;
&lt;li&gt;which tools are enabled for that host&lt;/li&gt;
&lt;li&gt;which OS user or service account executes side effects&lt;/li&gt;
&lt;li&gt;whether the human user must approve each mutating call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prefer &lt;strong&gt;tool allowlists&lt;/strong&gt; over "connect everything" MCP configs. A coding agent does not need payroll MCP servers on the same profile as a public support bot.&lt;/p&gt;

&lt;h3&gt;
  
  
  A2A agent access
&lt;/h3&gt;

&lt;p&gt;For each agent peer relationship, define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which caller agent IDs may invoke which skills&lt;/li&gt;
&lt;li&gt;maximum delegation depth&lt;/li&gt;
&lt;li&gt;which artifact types may cross the boundary&lt;/li&gt;
&lt;li&gt;whether user context must propagate as signed claims&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Map OAuth scopes (or equivalent) to &lt;strong&gt;skills&lt;/strong&gt;, not to blanket agent admin. Least privilege at the token layer beats hope at the prompt layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gateway-enforced vs per-agent policy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Per-agent policy&lt;/strong&gt; works when one team owns the whole graph and releases are coordinated. &lt;strong&gt;Gateway-enforced policy&lt;/strong&gt; works when multiple teams, tenants, or vendors share an agent network and you need one place to enforce allowlists, rate limits, and audit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    U[User / client] --&amp;gt; G[A2A gateway]
    G --&amp;gt; O[Orchestrator agent]
    O --&amp;gt;|A2A scoped token| S1[Specialist agent]
    O --&amp;gt;|A2A scoped token| S2[Specialist agent]
    S1 --&amp;gt; MG[MCP gateway]
    S2 --&amp;gt; MG
    MG --&amp;gt; T1[MCP tool servers]
    MG --&amp;gt; T2[MCP tool servers]
    G --&amp;gt; A[Audit log]
    MG --&amp;gt; A
    S1 --&amp;gt; A
    S2 --&amp;gt; A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A2A Gateway as the Control Plane
&lt;/h2&gt;

&lt;p&gt;An A2A gateway is not strictly required by the protocol, but it becomes necessary when agent traffic needs centralized governance.&lt;/p&gt;

&lt;p&gt;A gateway typically handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication termination and token exchange&lt;/li&gt;
&lt;li&gt;routing to the correct agent service by skill or tenant&lt;/li&gt;
&lt;li&gt;policy checks before tasks are accepted or forwarded&lt;/li&gt;
&lt;li&gt;protocol version negotiation&lt;/li&gt;
&lt;li&gt;rate limiting and abuse detection&lt;/li&gt;
&lt;li&gt;structured audit emission on every task transition&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When a gateway is overkill vs necessary
&lt;/h3&gt;

&lt;p&gt;A gateway is often overkill for a single orchestrator and two specialist agents in one Kubernetes namespace maintained by one team. It becomes necessary when partners invoke your agents, when multiple business units share infrastructure, when compliance requires uniform logging, or when you cannot trust every agent implementation to enforce policy correctly.&lt;/p&gt;

&lt;p&gt;Pair an &lt;strong&gt;A2A gateway&lt;/strong&gt; with an &lt;strong&gt;MCP gateway&lt;/strong&gt; (or MCP proxy) so tool access receives the same treatment: identity, allowlists, egress controls, and audit at the tool boundary rather than only at the chat UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partner-facing vs internal Agent Cards
&lt;/h3&gt;

&lt;p&gt;Publish different discovery metadata for external and internal callers. External cards expose narrow skills and stricter auth. Internal cards may list maintenance or admin skills but must never be reachable without stronger authentication than the public card implies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent Registry and Discovery Security
&lt;/h2&gt;

&lt;p&gt;Discovery is part of the attack surface. Anyone who controls what agents appear "available" controls where orchestrators send work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registry vs well-known Agent Card URLs
&lt;/h3&gt;

&lt;p&gt;Small deployments use well-known URLs per agent (&lt;code&gt;/.well-known/agent-card.json&lt;/code&gt;). Enterprise deployments add a &lt;strong&gt;registry&lt;/strong&gt; that indexes agent IDs, versions, endpoints, owners, and policy tags. The registry is a policy object: entries should record which tenants may discover which agents, not only where they live.&lt;/p&gt;

&lt;h3&gt;
  
  
  Versioning, deprecation, and ownership
&lt;/h3&gt;

&lt;p&gt;Registry records need owners, change history, and deprecation dates. An orchestrator that caches Agent Cards must refresh on TTL and verify signatures where supported. Stale cards are how retired skills keep receiving traffic long after a vulnerability is patched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise internal networks vs external partners
&lt;/h3&gt;

&lt;p&gt;Internal agent meshes can rely on mTLS and private DNS. Partner agents need explicit federation rules, contractually scoped skills, and stronger artifact inspection because you do not control their runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delegation Across Agent Boundaries
&lt;/h2&gt;

&lt;p&gt;Delegation is where A2A security is won or lost. When Agent A sends a task to Agent B, three questions must have crisp answers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Whose authority is being exercised?&lt;/strong&gt; The user's, A's service account, or a blended delegated token?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is B allowed to do with that authority?&lt;/strong&gt; Read-only analysis, or mutating tools on A's behalf?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who is accountable if B exceeds scope?&lt;/strong&gt; A, B, the gateway policy, or the human who approved an unclear prompt?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Propagating user intent vs over-delegation
&lt;/h3&gt;

&lt;p&gt;Pass &lt;strong&gt;signed delegation claims&lt;/strong&gt; that include user ID, original task ID, allowed skills, expiry, and maximum hop count. Downstream agents must reject tasks that expand scope silently. If B needs higher privilege than A held, transition to &lt;code&gt;input_required&lt;/code&gt; and obtain explicit human approval rather than upgrading tokens invisibly.&lt;/p&gt;

&lt;p&gt;Human-in-the-loop approval flows for risky delegation are covered in &lt;a href="https://www.glukhov.org/ai-systems/architecture/a2a-streaming-async-task-lifecycle/" rel="noopener noreferrer"&gt;A2A Streaming and Async Tasks for Long-Running Agent Workflows&lt;/a&gt; where &lt;code&gt;input_required&lt;/code&gt; is a first-class task state rather than an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant User
    participant Orch as Orchestrator agent
    participant GW as A2A gateway
    participant Spec as Specialist agent
    participant MCP as MCP tool server
    User-&amp;gt;&amp;gt;Orch: Request with user token
    Orch-&amp;gt;&amp;gt;GW: Delegate task (scoped delegation token)
    GW-&amp;gt;&amp;gt;GW: Policy check scope + hop count
    GW-&amp;gt;&amp;gt;Spec: Forward task (reduced scope token)
    Spec-&amp;gt;&amp;gt;MCP: Tool call (tool-scoped credential)
    MCP-&amp;gt;&amp;gt;MCP: Enforce allowlist + user context
    Spec--&amp;gt;&amp;gt;GW: Artifact + audit events
    GW--&amp;gt;&amp;gt;Orch: Task update
    Orch--&amp;gt;&amp;gt;User: Final response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Separate reasoning from execution permissions
&lt;/h3&gt;

&lt;p&gt;An agent may need broad &lt;strong&gt;read&lt;/strong&gt; access to plan while &lt;strong&gt;write&lt;/strong&gt; tools sit behind approval. Split credentials or use distinct MCP profiles for planning vs execution so a model mistake cannot immediately mutate production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit Trails and Answer Provenance
&lt;/h2&gt;

&lt;p&gt;If you cannot reconstruct a delegation chain, you cannot explain an incident, pass an audit, or dispute a billing anomaly.&lt;/p&gt;

&lt;p&gt;Log at three layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gateway:&lt;/strong&gt; authentication result, policy decision, routed agent ID, task ID, parent task ID, rate-limit events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent:&lt;/strong&gt; task state transitions, messages sent/received, model/tool invocations (arguments redacted as needed), artifacts created, delegation outward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP server:&lt;/strong&gt; tool name, caller agent ID, user context, success/failure, latency, rows affected or resource IDs (policy permitting).&lt;/p&gt;

&lt;p&gt;Correlate with &lt;strong&gt;trace ID&lt;/strong&gt; across all layers. &lt;a href="https://www.glukhov.org/observability/observability-for-llm-systems/" rel="noopener noreferrer"&gt;Observability for LLM Systems&lt;/a&gt; covers instrumentation backends; this article defines &lt;strong&gt;what&lt;/strong&gt; must be captured so those backends have meaningful signal.&lt;/p&gt;

&lt;p&gt;Final answer provenance should answer: which user, which orchestrator task, which specialist agents, which tools, which artifacts influenced the text the user saw, and which policy gates fired along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime Policy, Egress, and Secrets
&lt;/h2&gt;

&lt;p&gt;Runtime policy engines (OPA, Cedar, custom rule services) evaluate structured events: "tool X with args Y for user Z." They complement guardrails because they do not depend on the model behaving well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human approval&lt;/strong&gt; belongs in runtime policy for irreversible or high-cost actions: payments, external email, production config changes, privilege grants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Egress controls&lt;/strong&gt; limit which domains MCP servers and agents may call. An agent that can both read secrets and POST to arbitrary URLs is a data-loss waiting to happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets&lt;/strong&gt; never belong in Agent Cards or prompts. MCP hosts should inject short-lived credentials at execution time from a secrets manager. For transport encryption, key management, and baseline infra security patterns, see &lt;a href="https://www.glukhov.org/app-architecture/security/securing-data-at-rest-in-transit-runtime/" rel="noopener noreferrer"&gt;Architectural Patterns for Securing Data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Push notification webhooks in async A2A flows need the same rigor: verify sender identity, reject stale events, and never treat a webhook payload as authorization on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Security Architecture
&lt;/h2&gt;

&lt;p&gt;The following diagram summarizes a production-oriented layout for &lt;a href="https://www.glukhov.org/ai-systems/mcp/a2a-vs-mcp-ai-agent-protocols/" rel="noopener noreferrer"&gt;A2A outside, MCP inside&lt;/a&gt; deployments at scale.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
    subgraph Client layer
        U[User / API client]
    end
    subgraph Control plane
        GW[A2A gateway]
        REG[Agent registry]
        POL[Policy engine]
        AUD[Audit log]
        SEC[Secrets manager]
    end
    subgraph Agent layer
        OR[Orchestrator]
        SA[Specialist agents]
    end
    subgraph Tool layer
        MG[MCP gateway]
        MCP[MCP servers]
    end
    subgraph Observability
        OBS[Tracing + metrics]
    end
    U --&amp;gt; GW
    GW --&amp;gt; REG
    GW --&amp;gt; POL
    GW --&amp;gt; OR
    OR --&amp;gt; GW
    GW --&amp;gt; SA
    SA --&amp;gt; MG
    MG --&amp;gt; MCP
    POL --&amp;gt; GW
    POL --&amp;gt; MG
    SEC --&amp;gt; SA
    SEC --&amp;gt; MCP
    GW --&amp;gt; AUD
    MG --&amp;gt; AUD
    SA --&amp;gt; AUD
    AUD --&amp;gt; OBS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestrator sees specialist agents through A2A. Specialists see tools through MCP. Users never receive raw MCP credentials, and partners never receive internal skill surfaces without policy review.&lt;/p&gt;

&lt;p&gt;For protocol concepts (Agent Cards, tasks, artifacts), see &lt;a href="https://www.glukhov.org/ai-systems/architecture/a2a-protocol-explained/" rel="noopener noreferrer"&gt;What Is the A2A Protocol?&lt;/a&gt;. For adoption and enterprise framing, see &lt;a href="https://www.glukhov.org/ai-systems/comparisons/a2a-protocol-2026-adoption/" rel="noopener noreferrer"&gt;Google A2A Protocol in 2026&lt;/a&gt;. For topology when many agents coordinate, see &lt;a href="https://www.glukhov.org/ai-systems/architecture/multi-agent-orchestration-patterns/" rel="noopener noreferrer"&gt;Multi-Agent Orchestration Patterns&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Checklist for A2A and MCP Security
&lt;/h2&gt;

&lt;p&gt;Before exposing agent protocols beyond a trusted sandbox, verify:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity and auth&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] No anonymous agents in production paths&lt;/li&gt;
&lt;li&gt;[ ] Every MCP and A2A call authenticated on every request&lt;/li&gt;
&lt;li&gt;[ ] OAuth scopes or equivalent mapped to skills/tools, not blanket admin&lt;/li&gt;
&lt;li&gt;[ ] Public vs authenticated Agent Card views defined intentionally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Delegation and policy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Delegation tokens carry user ID, task ID, scope, expiry, hop limit&lt;/li&gt;
&lt;li&gt;[ ] Downstream agents reject scope expansion without explicit approval&lt;/li&gt;
&lt;li&gt;[ ] High-risk tools require runtime policy or human approval&lt;/li&gt;
&lt;li&gt;[ ] Reasoning and execution use separate credentials where possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Discovery and registry&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Agent registry entries have owners and version history&lt;/li&gt;
&lt;li&gt;[ ] Agent Cards refreshed on TTL; signatures verified where supported&lt;/li&gt;
&lt;li&gt;[ ] Partner agents federated with explicit skill allowlists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Audit and observability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Gateway, agent, and MCP layers emit correlated audit events&lt;/li&gt;
&lt;li&gt;[ ] Delegation chains logged with parent and child task IDs&lt;/li&gt;
&lt;li&gt;[ ] Artifact provenance recorded for final answers&lt;/li&gt;
&lt;li&gt;[ ] Trace IDs connect to observability backends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Abuse and resilience&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Rate limits per user, agent, and tenant&lt;/li&gt;
&lt;li&gt;[ ] Timeout policies on delegated tasks&lt;/li&gt;
&lt;li&gt;[ ] Egress allowlists on tool servers&lt;/li&gt;
&lt;li&gt;[ ] Secrets in a manager, not in cards, prompts, or repos&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A2A and MCP interoperability is powerful because agents and tools can compose across team and vendor boundaries, but that power is unsafe without identity, authorization, delegation limits, and audit design. Guardrails protect the model conversation; protocol security protects the actions agents take on behalf of users.&lt;/p&gt;

&lt;p&gt;Treat Agent Cards as advertisements, delegation as a signed contract, MCP tools as privileged code execution, and audit logs as the evidence chain you will need when something interesting happens at 2 a.m.&lt;/p&gt;

&lt;p&gt;Build the gateway when governance needs a single throat to choke. Split credentials before you split agents. Log every hop so the answer "the model decided" is never the final incident report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between LLM guardrails and A2A MCP agent security?&lt;/strong&gt;&lt;br&gt;
Guardrails constrain model input and output. Protocol security constrains who may invoke tools, delegate tasks, and act on whose behalf across MCP and A2A with identity, authorization, and audit trails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should agent identity work in an A2A deployment?&lt;/strong&gt;&lt;br&gt;
Separate human, agent service, and task identities. Validate credentials on every request, use scoped tokens, and treat Agent Cards as discovery metadata rather than proof of trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the confused deputy problem in multi-agent systems?&lt;/strong&gt;&lt;br&gt;
It occurs when a privileged agent or tool performs a sensitive action because a less privileged caller smuggled instructions through delegation or artifacts. Enforce scope at every hop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need an A2A gateway in production?&lt;/strong&gt;&lt;br&gt;
Single-team internal deployments may enforce policy per agent. Multi-tenant, multi-vendor, or partner-facing networks usually need a gateway for centralized auth, routing, rate limits, and audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should an A2A MCP audit log contain?&lt;/strong&gt;&lt;br&gt;
User ID, agent ID, task ID, parent task ID, tool calls, policy decisions, artifacts, and timestamps correlated with trace IDs across gateway, agent, and MCP layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A2A Protocol -- Enterprise-ready security topics: &lt;a href="https://github.com/a2aproject/A2A/blob/main/docs/topics/enterprise-ready.md" rel="noopener noreferrer"&gt;https://github.com/a2aproject/A2A/blob/main/docs/topics/enterprise-ready.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A2A Protocol -- Specification overview: &lt;a href="https://a2a-protocol.org/latest/specification/" rel="noopener noreferrer"&gt;https://a2a-protocol.org/latest/specification/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A2A Protocol -- Streaming and push notification security: &lt;a href="https://a2a-protocol.org/latest/topics/streaming-and-async/" rel="noopener noreferrer"&gt;https://a2a-protocol.org/latest/topics/streaming-and-async/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>A2A Streaming and Async Tasks for Long-Running Agent Workflows</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Fri, 10 Jul 2026 09:36:29 +0000</pubDate>
      <link>https://dev.to/rosgluk/a2a-streaming-and-async-tasks-for-long-running-agent-workflows-1193</link>
      <guid>https://dev.to/rosgluk/a2a-streaming-and-async-tasks-for-long-running-agent-workflows-1193</guid>
      <description>&lt;p&gt;Most AI agent demos still behave like chat completions with extra steps: you send a prompt, wait a few seconds, and get an answer back in one response.&lt;/p&gt;

&lt;p&gt;Real agent work often does not fit that pattern. Research, code review, procurement analysis, incident investigation, and multi-step planning can run for minutes or hours, and they may need clarification halfway through, stream partial results, delegate to another agent, and produce files rather than a single text reply. That is where the A2A protocol's async model matters within the broader &lt;a href="https://www.glukhov.org/ai-systems/" rel="noopener noreferrer"&gt;AI Systems&lt;/a&gt; cluster, because A2A treats long-running work as a &lt;strong&gt;Task&lt;/strong&gt; with a lifecycle instead of a one-shot HTTP response. Clients can stay connected via Server-Sent Events (SSE), poll task state, or register push webhooks when they cannot hold a connection open.&lt;/p&gt;

&lt;p&gt;This article covers operational design for those workflows, including when to stream versus poll versus push, how &lt;code&gt;input_required&lt;/code&gt; fits human-in-the-loop flows, failure handling, and what to instrument in production. For Agent Cards, messages, parts, and the full task model, see &lt;a href="https://www.glukhov.org/ai-systems/architecture/a2a-protocol-explained/" rel="noopener noreferrer"&gt;What Is the A2A Protocol? Agent Cards and Tasks Explained&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Long-Running A2A Agent Tasks Need Async Design
&lt;/h2&gt;

&lt;p&gt;A synchronous request/response mental model breaks down quickly once agent work spans tools, delegation, approvals, and large artifacts. An agent task may call multiple MCP servers internally, delegate sub-work to another agent over A2A, wait for human approval, generate large artifacts in chunks, fail partway through and need partial recovery, and accumulate token cost across several hops. HTTP APIs can approximate this with timeouts, background jobs, and ad hoc status endpoints, but A2A bakes task identity and state into the protocol so clients and gateways can reason about work consistently. For how those layers fit inside a production assistant before you add async A2A boundaries, see &lt;a href="https://www.glukhov.org/ai-systems/architecture/ai-assistant-architecture/" rel="noopener noreferrer"&gt;AI Assistant Architecture: LLM, Memory, Tools, Routing, Observability&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My bias is practical: &lt;strong&gt;do not create a Task for everything&lt;/strong&gt;, because a one-line summary does not need a lifecycle. Use a Task when work is stateful, auditable, long-running, artifact-producing, or may need input mid-flight. The rule of thumb from the explainer still holds: simple interactions can return a Message, while complex work should return a Task.&lt;/p&gt;

&lt;h2&gt;
  
  
  A2A Task Lifecycle and State Transitions
&lt;/h2&gt;

&lt;p&gt;An A2A Task moves through states that clients can query at any time. Exact naming varies slightly by implementation, but the model is stable across servers that follow the protocol.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; submitted
    submitted --&amp;gt; working
    working --&amp;gt; input_required
    input_required --&amp;gt; working
    working --&amp;gt; completed
    working --&amp;gt; failed
    working --&amp;gt; canceled
    working --&amp;gt; rejected
    submitted --&amp;gt; rejected
    input_required --&amp;gt; failed
    input_required --&amp;gt; canceled
    completed --&amp;gt; [*]
    failed --&amp;gt; [*]
    canceled --&amp;gt; [*]
    rejected --&amp;gt; [*]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;submitted&lt;/strong&gt; state means the client sent work and the agent accepted or queued it. In &lt;strong&gt;working&lt;/strong&gt;, the agent is actively processing, which may include tool calls, delegation, or streaming partial output. The &lt;strong&gt;input_required&lt;/strong&gt; state indicates the agent paused because it needs more input, clarification, or human approval, and it is not a failure state. &lt;strong&gt;completed&lt;/strong&gt; is terminal success with artifacts available; &lt;strong&gt;failed&lt;/strong&gt; is a terminal error whose details and partial artifacts depend on implementation; &lt;strong&gt;canceled&lt;/strong&gt; means a client, gateway, or authorized caller stopped the task; and &lt;strong&gt;rejected&lt;/strong&gt; means the agent refused the task because of policy, capability mismatch, or auth.&lt;/p&gt;

&lt;h3&gt;
  
  
  When input_required pauses versus fails a workflow
&lt;/h3&gt;

&lt;p&gt;Treat &lt;code&gt;input_required&lt;/code&gt; as a deliberate &lt;strong&gt;pause&lt;/strong&gt;, not an exception. The agent is telling you it cannot proceed without something from you, whether that is a missing parameter, a policy confirmation, or a manager sign-off on a high-risk action. A workflow &lt;strong&gt;fails&lt;/strong&gt; when the task reaches &lt;code&gt;failed&lt;/code&gt; or &lt;code&gt;rejected&lt;/code&gt;, or when a caller exceeds a timeout waiting for input that never arrives, so you should design explicit timeouts for human steps rather than letting approvals sit indefinitely.&lt;/p&gt;

&lt;p&gt;An approval that waits three days without escalation is a stuck workflow, not a patient one, and stuck workflows clog task stores while making observability dashboards harder to read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who can cancel an A2A task
&lt;/h3&gt;

&lt;p&gt;Cancellation authority should be defined at design time rather than debated during an incident. The &lt;strong&gt;client&lt;/strong&gt; usually can cancel tasks it created; a &lt;strong&gt;gateway&lt;/strong&gt; may cancel on behalf of tenants, policy violations, or budget limits; and an &lt;strong&gt;upstream agent&lt;/strong&gt; may cancel delegated work when orchestrating over A2A if the protocol and policy allow it. Log who canceled and why, because in multi-agent chains orphan work is a common source of surprise token bills.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human-in-the-Loop with input_required Task States
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;input_required&lt;/code&gt; is one of A2A's most underused design features, and many teams treat it as an error code when it is actually a first-class workflow state. In production you will hit cases where the agent &lt;strong&gt;should&lt;/strong&gt; stop, such as spending budget on an ambiguous request, executing an irreversible action, accessing sensitive data without scope confirmation, or delegating to a specialist that needs explicit user intent. Model these as deliberate transitions to &lt;code&gt;input_required&lt;/code&gt;, with a clear message explaining what is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approval flows for risky A2A delegation
&lt;/h3&gt;

&lt;p&gt;When Agent A delegates to Agent B over A2A and Agent B enters &lt;code&gt;input_required&lt;/code&gt; for human approval, three systems need to agree on what happens next. The downstream agent pauses and exposes what it needs, the orchestrator or gateway surfaces that pause to the user, and the user's response resumes the task via a new message. The &lt;a href="https://www.glukhov.org/ai-systems/mcp/a2a-vs-mcp-ai-agent-protocols/" rel="noopener noreferrer"&gt;A2A vs MCP&lt;/a&gt; comparison explains why delegation across agent boundaries is a different problem from tool access, and why approval semantics belong at the task layer rather than inside a single MCP call. Do not silently auto-approve because the UX is inconvenient, since expensive mistakes usually come from convenience shortcuts rather than from missing models.&lt;/p&gt;

&lt;h3&gt;
  
  
  UX patterns for paused A2A tasks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Blocking wait&lt;/strong&gt; means the UI shows a spinner or approval card until the task leaves &lt;code&gt;input_required&lt;/code&gt;, which works well for short human steps. &lt;strong&gt;Non-blocking wait&lt;/strong&gt; means the client records the task ID, lets the user continue elsewhere, and uses polling or push to notify when input is needed again, which is required for mobile, email-linked approvals, or multi-tab assistants. &lt;strong&gt;Timeout when humans are slow&lt;/strong&gt; means defining an SLA per step and, after N hours, transitioning to &lt;code&gt;failed&lt;/code&gt; or escalating to another queue, because unbounded waits clog task stores and confuse observability dashboards.&lt;/p&gt;

&lt;h3&gt;
  
  
  How an A2A gateway handles input_required
&lt;/h3&gt;

&lt;p&gt;If you run an A2A gateway, decide whether it forwards &lt;code&gt;input_required&lt;/code&gt; events transparently, aggregates pauses from multiple downstream agents into one user prompt, or enforces that certain skills always require approval before leaving &lt;code&gt;input_required&lt;/code&gt;. Auth and policy for approved actions belong in a dedicated security article; for now, assume every resumed task should carry the same user identity and scope as the original request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing Sync, SSE Streaming, Polling, or Push Notifications
&lt;/h2&gt;

&lt;p&gt;A2A supports multiple interaction modes, and the right choice depends on client capabilities and latency needs rather than on which mode sounds most modern.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Client requirements&lt;/th&gt;
&lt;th&gt;Tradeoffs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sync (SendMessage, short Task)&lt;/td&gt;
&lt;td&gt;Quick work, immediate Messages&lt;/td&gt;
&lt;td&gt;Simple HTTP client&lt;/td&gt;
&lt;td&gt;Timeouts on slow agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSE streaming&lt;/td&gt;
&lt;td&gt;Live progress, incremental artifacts&lt;/td&gt;
&lt;td&gt;Long-lived connection&lt;/td&gt;
&lt;td&gt;Proxies, mobile background limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polling (GetTask)&lt;/td&gt;
&lt;td&gt;Batch clients, simple integrations&lt;/td&gt;
&lt;td&gt;Timer + task ID&lt;/td&gt;
&lt;td&gt;Higher latency, more requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push webhooks&lt;/td&gt;
&lt;td&gt;Mobile, serverless, multi-hour jobs&lt;/td&gt;
&lt;td&gt;HTTPS receiver + verification&lt;/td&gt;
&lt;td&gt;Async complexity, security hardening&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Read Agent Card capability flags first
&lt;/h3&gt;

&lt;p&gt;Before choosing a mode, read the agent's &lt;strong&gt;Agent Card&lt;/strong&gt;, because streaming requires &lt;code&gt;capabilities.streaming: true&lt;/code&gt; and push notification support is advertised separately. Clients that assume every agent streams will break against minimal implementations, so negotiation is not ceremonial: it prevents runtime failures when a specialist agent only supports poll-based status checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to use assistant-side polling around A2A
&lt;/h3&gt;

&lt;p&gt;Your assistant runtime may wrap A2A task polling in a scheduler loop rather than exposing raw protocol details to the user. That pattern overlaps with general &lt;strong&gt;polling agents&lt;/strong&gt;, which are background processes that wake up, check state, and act. For durable scheduling, idempotency, and queue patterns outside A2A specifically, see &lt;a href="https://www.glukhov.org/ai-systems/architecture/polling-agents-ai-assistants-implementation-patterns/" rel="noopener noreferrer"&gt;Polling Agents in AI Assistants: 11 Implementation Patterns&lt;/a&gt;. Use assistant polling when you orchestrate many A2A tasks from a single control plane, and use native A2A streaming or push when the client connects directly to the agent boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A2A Server-Sent Events (SSE) Streaming
&lt;/h2&gt;

&lt;p&gt;SSE is A2A's primary real-time channel. The client calls &lt;strong&gt;SendStreamingMessage&lt;/strong&gt;, opens an HTTP connection, and receives a &lt;code&gt;text/event-stream&lt;/code&gt; response until the task reaches a terminal or interrupted state. Each event's payload is JSON-RPC-shaped, and typical result types include a &lt;strong&gt;Task&lt;/strong&gt; snapshot, a &lt;strong&gt;TaskStatusUpdateEvent&lt;/strong&gt; for lifecycle transitions and intermediate agent messages, and a &lt;strong&gt;TaskArtifactUpdateEvent&lt;/strong&gt; for chunked artifact delivery with &lt;code&gt;append&lt;/code&gt; and &lt;code&gt;lastChunk&lt;/code&gt; hints for reassembly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant Client
    participant A2A Server
    Client-&amp;gt;&amp;gt;A2A Server: SendStreamingMessage
    A2A Server--&amp;gt;&amp;gt;Client: HTTP 200 text/event-stream
    loop Until terminal or input_required
        A2A Server--&amp;gt;&amp;gt;Client: TaskStatusUpdateEvent
        A2A Server--&amp;gt;&amp;gt;Client: TaskArtifactUpdateEvent (optional)
    end
    A2A Server--&amp;gt;&amp;gt;Client: Close stream
    Note over Client,A2A Server: On disconnect before terminal state,&amp;lt;br/&amp;gt;client may call SubscribeToTask
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Streaming progress updates and partial artifacts
&lt;/h3&gt;

&lt;p&gt;Streaming shines when users should &lt;strong&gt;see work happening&lt;/strong&gt;, whether that means step counters ("3 of 7 sources reviewed"), partial text generation, incremental file chunks for large reports, or state transitions from &lt;code&gt;working&lt;/code&gt; to &lt;code&gt;input_required&lt;/code&gt; without polling. Design UI around event types rather than around a single final blob, because if you only display output when &lt;code&gt;completed&lt;/code&gt; arrives you might as well poll.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSE connection drops and resubscription
&lt;/h3&gt;

&lt;p&gt;Networks drop, laptops sleep, and load balancers idle-timeout SSE connections, so long streams need recovery logic rather than optimistic assumptions. A2A provides &lt;strong&gt;SubscribeToTask&lt;/strong&gt; so clients can reconnect to an in-progress task stream, and your client SDK should persist &lt;code&gt;taskId&lt;/code&gt; locally, detect stream closure before terminal state, resubscribe with backoff, and de-duplicate events if the server replays overlapping state. Without resubscription logic, long tasks feel fragile in production even when the agent backend is healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A2A Push Notifications and Webhooks
&lt;/h2&gt;

&lt;p&gt;Push fits scenarios where SSE is a poor match, such as mobile apps in the background, serverless handlers, or tasks that run for hours or days. The client supplies a &lt;strong&gt;PushNotificationConfig&lt;/strong&gt; with a &lt;code&gt;url&lt;/code&gt; (HTTPS webhook on the client side), an optional &lt;code&gt;token&lt;/code&gt; for validating incoming POSTs, and optional &lt;code&gt;authentication&lt;/code&gt; details for how the A2A server authenticates to the webhook. Configuration can ride along with the initial SendMessage or SendStreamingMessage call, or be added later via &lt;strong&gt;CreateTaskPushNotificationConfig&lt;/strong&gt; for an existing task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant Client
    participant A2A Server
    participant Webhook
    Client-&amp;gt;&amp;gt;A2A Server: SendMessage + PushNotificationConfig
    A2A Server--&amp;gt;&amp;gt;Client: taskId
    Note over A2A Server: Task runs asynchronously
    A2A Server-&amp;gt;&amp;gt;Webhook: POST state change notification
    Webhook-&amp;gt;&amp;gt;A2A Server: GetTask(taskId)
    A2A Server--&amp;gt;&amp;gt;Webhook: Updated Task + artifacts
    Webhook-&amp;gt;&amp;gt;Client: Resume workflow / notify user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a significant update occurs, the A2A server POSTs to the webhook and the client typically calls &lt;strong&gt;GetTask&lt;/strong&gt; with the notified &lt;code&gt;taskId&lt;/code&gt; to fetch the full updated Task and artifacts. Push is a &lt;strong&gt;signal&lt;/strong&gt;, not a full payload transport.&lt;/p&gt;

&lt;h3&gt;
  
  
  When push beats an open SSE connection
&lt;/h3&gt;

&lt;p&gt;Prefer push when the client cannot maintain SSE (mobile, edge functions), when updates are infrequent and milestone-based rather than token-by-token, or when you want the server to wake a disconnected workflow engine. Prefer SSE when users watch progress live, when artifacts stream in many small chunks, or when latency below a few seconds matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correlating push notifications to A2A tasks
&lt;/h3&gt;

&lt;p&gt;Every push handler should log and propagate the &lt;code&gt;taskId&lt;/code&gt;, a trace or correlation ID from the original request, the event type or state transition, and a timestamp from the notification so stale events can be rejected. Replay attacks and duplicate deliveries happen in production, so idempotent handlers are not optional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Push endpoint security overview
&lt;/h3&gt;

&lt;p&gt;Push introduces SSRF risk on the server when malicious clients register internal URLs, and impersonation risk on the client when fake POSTs arrive at the webhook. Mitigations include URL allowlists, ownership verification, signed JWTs with JWKS, timestamp checks, and validating the config token. The full threat model, identity layers, and gateway controls live in &lt;a href="https://www.glukhov.org/llm-architecture/guardrails/a2a-mcp-agent-security/" rel="noopener noreferrer"&gt;A2A and MCP Agent Security: Identity, Delegation, and Audit Trails&lt;/a&gt;; until you have read it, treat webhook verification with the same seriousness as payment callbacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async A2A Workflow Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fire-and-follow task submission
&lt;/h3&gt;

&lt;p&gt;The client submits a task, receives a task ID immediately, and disconnects, then later polls GetTask or waits for push. This is the default pattern for serverless and batch pipelines, but you should persist the task ID in durable storage before acknowledging the user, because serverless invocations that forget the ID lose the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resuming a task after input_required
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;input_required&lt;/code&gt;, the user sends a new message against the same task and the agent transitions back to &lt;code&gt;working&lt;/code&gt;. Design messages so resumption context is explicit, because "Approved: proceed with vendor X" beats a bare "yes" when you need to audit what was approved six hours later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chained A2A delegation with intermediate artifacts
&lt;/h3&gt;

&lt;p&gt;Consider a research workflow where an orchestrator owns Task T1 and delegates retrieval, summarization, and verification to specialist agents, each with its own task ID and artifacts along the way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    U[User] --&amp;gt; O[Orchestrator Task T1]
    O --&amp;gt;|A2A| R[Retrieval agent T2]
    R --&amp;gt; A2[artifact: raw sources]
    O --&amp;gt;|A2A| S[Summarization agent T3]
    S --&amp;gt; A3[artifact: draft summary]
    O --&amp;gt;|A2A| V[Verification agent T4]
    V --&amp;gt; A4[artifact: fact-check report]
    O --&amp;gt; F[final artifact: recommendation memo]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each hop has its own task ID and state machine, so the orchestrator should stream or poll downstream tasks independently, persist intermediate artifacts before starting the next hop, and fail gracefully if T3 completes but T4 rejects the draft. &lt;a href="https://www.glukhov.org/ai-systems/architecture/multi-agent-orchestration-patterns/" rel="noopener noreferrer"&gt;Multi-Agent Orchestration Patterns&lt;/a&gt; covers topology choice when those specialists run as separate services rather than in one runtime. Partial progress is valuable, and a failed verification should not delete a usable draft without a clear reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  Durable task storage for delayed completion
&lt;/h3&gt;

&lt;p&gt;Task state and artifacts should survive process restarts. If your agent runs in Kubernetes, assume pods die mid-task and back task records and artifact blobs to a store the agent container does not own exclusively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Handling for Long-Running A2A Workflows
&lt;/h2&gt;

&lt;p&gt;Long-running workflows fail in predictable ways through timeouts, retries, partial artifacts, and unsafe cancellation, and each needs an explicit policy rather than ad hoc handling in client code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-hop and end-to-end timeout budgets
&lt;/h3&gt;

&lt;p&gt;Set timeouts at two levels: a &lt;strong&gt;per-hop&lt;/strong&gt; maximum for one agent task before escalation or cancel, and an &lt;strong&gt;end-to-end&lt;/strong&gt; maximum for the user-visible workflow. A retrieval agent that hangs should not block the entire orchestrator until the user's browser times out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retries and idempotency for A2A tasks
&lt;/h3&gt;

&lt;p&gt;Retries without idempotency duplicate side effects such as double charges, duplicate tickets, and repeated emails. Use stable client message IDs or idempotency keys where the protocol allows, and for business mutations align with &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/idempotency-in-distributed-systems/" rel="noopener noreferrer"&gt;Idempotency in Distributed Systems That Actually Works&lt;/a&gt;. Retry only &lt;strong&gt;transient&lt;/strong&gt; failures like network blips or 503s, and do not retry &lt;code&gt;rejected&lt;/code&gt; or policy failures blindly because you will amplify cost and annoy downstream agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partial artifact recovery policies
&lt;/h3&gt;

&lt;p&gt;When a task fails after producing partial artifacts, define whether you expose partial output to the user with a clear "incomplete" label, allow resume from the last good checkpoint, or discard partial output when it could mislead in medical, legal, or financial contexts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe cancellation across delegation chains
&lt;/h3&gt;

&lt;p&gt;Cancel downstream tasks when an upstream user aborts, use a delegation graph so cancel propagates, and log canceled tasks that already incurred cost because finance teams notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability for Async A2A Workflows
&lt;/h2&gt;

&lt;p&gt;You cannot debug multi-agent async work unless you can trace it across boundaries, which means correlating identifiers on every hop rather than relying on unstructured logs. Minimum correlation fields include a &lt;strong&gt;trace ID&lt;/strong&gt; per user-initiated workflow, a &lt;strong&gt;task ID&lt;/strong&gt; per agent task including delegated children, an &lt;strong&gt;agent ID&lt;/strong&gt; for the Agent Card or service that handled the hop, and a &lt;strong&gt;parent task ID&lt;/strong&gt; that links delegation chains.&lt;/p&gt;

&lt;p&gt;Log every state transition with timestamps, and log artifact creation events with size and hash rather than necessarily full content when PII policies apply. Attribute &lt;strong&gt;cost and latency per hop&lt;/strong&gt;, because multi-agent workflows hide token spend until the bill arrives and per-task cost labels make "which specialist is expensive?" answerable. For metrics, tracing backends, and LLM-specific instrumentation patterns, see &lt;a href="https://www.glukhov.org/observability/observability-for-llm-systems/" rel="noopener noreferrer"&gt;Observability for LLM Systems&lt;/a&gt; and the broader &lt;a href="https://www.glukhov.org/observability/" rel="noopener noreferrer"&gt;Observability&lt;/a&gt; pillar for how those signals fit into a production telemetry stack.&lt;br&gt;
When a user asks "why did the agent do that?", your answer should be a trace spanning orchestrator, A2A hops, MCP tool calls, and any &lt;code&gt;input_required&lt;/code&gt; pauses rather than a shrug and a log grep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Checklist for A2A Streaming and Async Tasks
&lt;/h2&gt;

&lt;p&gt;Before shipping long-running A2A paths to production, verify the following areas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Card and capabilities&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;capabilities.streaming&lt;/code&gt; reflects actual SSE support&lt;/li&gt;
&lt;li&gt;[ ] Push notification support documented if implemented&lt;/li&gt;
&lt;li&gt;[ ] Skills that require human approval document expected &lt;code&gt;input_required&lt;/code&gt; behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Client modes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] SSE client handles resubscription via SubscribeToTask&lt;/li&gt;
&lt;li&gt;[ ] Poll interval backs off under load&lt;/li&gt;
&lt;li&gt;[ ] Push webhook verifies authenticity and rejects stale events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Durability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Task state survives agent process restarts&lt;/li&gt;
&lt;li&gt;[ ] Artifacts stored outside ephemeral container filesystem&lt;/li&gt;
&lt;li&gt;[ ] Intermediate artifacts available for partial recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Failure and policy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Per-hop and end-to-end timeout budgets defined&lt;/li&gt;
&lt;li&gt;[ ] Retries idempotent for mutating operations&lt;/li&gt;
&lt;li&gt;[ ] Cancel propagates across delegation edges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] trace ID + task ID + agent ID on every hop&lt;/li&gt;
&lt;li&gt;[ ] State transitions logged&lt;/li&gt;
&lt;li&gt;[ ] Cost attribution per task or per agent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Load testing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] SSE through your reverse proxy (buffering breaks streams)&lt;/li&gt;
&lt;li&gt;[ ] Concurrent long tasks without memory leaks on open connections&lt;/li&gt;
&lt;li&gt;[ ] Push flood handling without webhook overload&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A2A's value shows up most clearly when work &lt;strong&gt;does not&lt;/strong&gt; fit a single synchronous API call, because streaming, async tasks, push notifications, and explicit task states are how the protocol handles real agent workloads such as research, delegation, approvals, and large artifacts without pretending everything completes in one HTTP round trip. Start with the simplest mode that works, add SSE when users need live progress, add push when connections cannot stay open, treat &lt;code&gt;input_required&lt;/code&gt; as a first-class design tool rather than a failure, and instrument every hop so multi-agent async workflows do not outrun your ability to explain them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When should you use A2A streaming instead of polling?&lt;/strong&gt;&lt;br&gt;
Use streaming when the client can hold an open HTTP connection and you need low-latency progress updates or incremental artifacts. Use polling when connections are unreliable, clients are batch-oriented, or you only need periodic status checks on long-running tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does input_required mean in an A2A task?&lt;/strong&gt;&lt;br&gt;
It is a pause state where the agent needs more information or human approval. Design UX and timeouts around it explicitly rather than treating it as an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do A2A push notifications work?&lt;/strong&gt;&lt;br&gt;
Register a PushNotificationConfig with an HTTPS webhook. The server POSTs on significant updates; the client calls GetTask to retrieve full state and artifacts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should you retry failed A2A tasks?&lt;/strong&gt;&lt;br&gt;
Retry transient failures with idempotency keys, respect timeout budgets, and do not blindly retry terminal states like rejected or policy failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should you log for long-running A2A workflows?&lt;/strong&gt;&lt;br&gt;
Correlate trace ID, task ID, and agent ID across hops. Log state transitions, artifacts, delegation, approvals, and per-step cost so you can reconstruct the full workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A2A Protocol -- Streaming and Asynchronous Operations: &lt;a href="https://a2a-protocol.org/latest/topics/streaming-and-async/" rel="noopener noreferrer"&gt;https://a2a-protocol.org/latest/topics/streaming-and-async/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A2A Protocol -- Specification overview: &lt;a href="https://a2a-protocol.org/latest/specification/" rel="noopener noreferrer"&gt;https://a2a-protocol.org/latest/specification/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>llm</category>
      <category>ai</category>
      <category>aicoding</category>
    </item>
    <item>
      <title>Run Docker Compose as a Linux Service with systemd</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Thu, 09 Jul 2026 11:37:13 +0000</pubDate>
      <link>https://dev.to/rosgluk/run-docker-compose-as-a-linux-service-with-systemd-2g8e</link>
      <guid>https://dev.to/rosgluk/run-docker-compose-as-a-linux-service-with-systemd-2g8e</guid>
      <description>&lt;p&gt;Docker Compose on a Linux server should start on boot, stop cleanly on shutdown, and survive reboots without manual intervention.&lt;/p&gt;

&lt;p&gt;Docker Compose is not Kubernetes, and that is fine for the workloads this guide targets. For many real systems, a Compose project on a single Linux host is the right amount of infrastructure — simple, readable, easy to back up, and good enough for internal tools, side projects, self-hosted services, staging environments, small production apps, and developer infrastructure.&lt;/p&gt;

&lt;p&gt;The missing piece is usually service management. Running this manually is not enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single command starts the stack, but it does not document how the stack should start on boot, stop during shutdown, reload after changes, write logs, recover from failures, or get updated safely. That is where systemd helps.&lt;/p&gt;

&lt;p&gt;This guide walks through running a Docker Compose project as a Linux service with systemd — unit files, boot ordering, updates, logs, and backups. The split of responsibility is deliberate: Docker runs containers, Compose defines the stack, and systemd starts and stops the project on the host. It is part of &lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools - a Guide to Development Workflows&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Docker Compose as a Service Makes Sense
&lt;/h2&gt;

&lt;p&gt;Running Compose under systemd makes sense when you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A single Linux server&lt;/li&gt;
&lt;li&gt;A small self-hosted application&lt;/li&gt;
&lt;li&gt;A reverse proxy stack&lt;/li&gt;
&lt;li&gt;A monitoring stack&lt;/li&gt;
&lt;li&gt;A local development platform&lt;/li&gt;
&lt;li&gt;An internal tool&lt;/li&gt;
&lt;li&gt;A staging environment&lt;/li&gt;
&lt;li&gt;A simple production service with known limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nginx Proxy Manager&lt;/li&gt;
&lt;li&gt;Traefik&lt;/li&gt;
&lt;li&gt;Gitea&lt;/li&gt;
&lt;li&gt;Grafana and Prometheus&lt;/li&gt;
&lt;li&gt;PostgreSQL plus a small web app&lt;/li&gt;
&lt;li&gt;Uptime Kuma&lt;/li&gt;
&lt;li&gt;Home Assistant helper services&lt;/li&gt;
&lt;li&gt;Private registry&lt;/li&gt;
&lt;li&gt;Internal API plus worker plus Redis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compose is a good fit when the operational model is still understandable by one person reading one directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Docker Compose Is Not Enough
&lt;/h2&gt;

&lt;p&gt;Use something else when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-node scheduling&lt;/li&gt;
&lt;li&gt;Automatic rescheduling across hosts&lt;/li&gt;
&lt;li&gt;Cluster-level service discovery&lt;/li&gt;
&lt;li&gt;Horizontal autoscaling&lt;/li&gt;
&lt;li&gt;Rolling deployments across many machines&lt;/li&gt;
&lt;li&gt;Fine-grained workload identity&lt;/li&gt;
&lt;li&gt;Complex network policy&lt;/li&gt;
&lt;li&gt;Large multi-team platform operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, Kubernetes, Nomad, Swarm, or a managed platform may be a better fit.&lt;/p&gt;

&lt;p&gt;My practical rule is to avoid using Kubernetes just to skip learning systemd, and to avoid using Compose when the workload clearly needs orchestration across multiple hosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basic Architecture
&lt;/h2&gt;

&lt;p&gt;A clean setup separates project files, the systemd unit, and persistent data on the host. The Compose project lives under &lt;code&gt;/opt/myapp/&lt;/code&gt; with &lt;code&gt;compose.yaml&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;data/&lt;/code&gt;, &lt;code&gt;backups/&lt;/code&gt;, and optional scripts such as &lt;code&gt;scripts/update.sh&lt;/code&gt;. The systemd unit file sits at &lt;code&gt;/etc/systemd/system/myapp.service&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
  subgraph host["Linux host"]
    systemd["systemd unit\n/etc/systemd/system/myapp.service"]
    compose["Docker Compose\n/opt/myapp/compose.yaml"]
    docker["Docker Engine"]
    fs["Persistent data\n/opt/myapp/data/"]
  end
  systemd --&amp;gt;|"ExecStart: docker compose up -d"| compose
  compose --&amp;gt; docker
  docker --&amp;gt; fs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer has a clear job: Docker runs containers, Compose defines the application stack, systemd starts and stops the Compose project on boot and shutdown, the host filesystem stores persistent data, backups stay explicit, and updates go through scripted, reviewable steps. This layout is deliberately boring, because boring infrastructure is easier to repair when something breaks at 2 a.m.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prepare the Compose Project Directory
&lt;/h2&gt;

&lt;p&gt;Create a directory under &lt;code&gt;/opt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/myapp
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /opt/myapp
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a Compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./html:/usr/share/nginx/html:ro&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nginx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-t&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the content directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; html
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello from Docker Compose"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; html/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test manually first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose ps
docker compose logs &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then stop it before handing lifecycle to systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not create a systemd service until the Compose project works manually. While you test, keep the &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-cheatsheet/" rel="noopener noreferrer"&gt;Docker Compose Cheatsheet&lt;/a&gt; nearby for &lt;code&gt;ps&lt;/code&gt;, &lt;code&gt;logs&lt;/code&gt;, &lt;code&gt;pull&lt;/code&gt;, and project structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the Modern &lt;code&gt;docker compose&lt;/code&gt; Command
&lt;/h2&gt;

&lt;p&gt;Docker Engine and the Compose plugin must be installed before you write a unit file. On Ubuntu, &lt;a href="https://www.glukhov.org/developer-tools/containers/install-docker-on-ubuntu/" rel="noopener noreferrer"&gt;Install Docker on Ubuntu&lt;/a&gt; walks through APT, Snap, rootless mode, and post-install security so you end up with a working &lt;code&gt;docker compose&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;Use this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old &lt;code&gt;docker-compose&lt;/code&gt; binary still exists on many machines, but modern Docker uses Compose as a Docker CLI plugin.&lt;/p&gt;

&lt;p&gt;In service files and scripts, prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/bin/docker compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can find the Docker path with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usually it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/bin/docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create a systemd Service for Docker Compose
&lt;/h2&gt;

&lt;p&gt;If unit files are new to you, &lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/executable-as-a-service-in-linux/" rel="noopener noreferrer"&gt;Run any Executable as a Service in Linux&lt;/a&gt; explains &lt;code&gt;Type&lt;/code&gt;, &lt;code&gt;ExecStart&lt;/code&gt;, &lt;code&gt;systemctl&lt;/code&gt;, and the general systemd workflow. This section applies those patterns specifically to a Compose stack.&lt;/p&gt;

&lt;p&gt;Create the service file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/systemd/system/myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;MyApp Docker Compose stack&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/myapp&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose down&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable it on boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Type=oneshot and RemainAfterExit=yes?
&lt;/h2&gt;

&lt;p&gt;This is the part many guides get subtly wrong.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker compose up -d&lt;/code&gt; starts containers in detached mode and exits, so there is no long-running foreground Compose process for systemd to supervise. The systemd unit should not pretend that &lt;code&gt;docker compose up -d&lt;/code&gt; is a long-running daemon.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells systemd:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run the start command.&lt;/li&gt;
&lt;li&gt;Consider the unit active after the command exits successfully.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;ExecStop&lt;/code&gt; when the service is stopped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matches the actual behavior of detached Compose, which is why &lt;code&gt;Type=oneshot&lt;/code&gt; with &lt;code&gt;RemainAfterExit=yes&lt;/code&gt; is the right default for most stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Type=simple?
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;Type=simple&lt;/code&gt;, systemd expects the &lt;code&gt;ExecStart&lt;/code&gt; process to keep running, but &lt;code&gt;docker compose up -d&lt;/code&gt; exits after starting containers. That can make systemd think the service ended, then call stop logic or mark the unit inactive depending on configuration.&lt;/p&gt;

&lt;p&gt;If you want &lt;code&gt;Type=simple&lt;/code&gt;, you would usually run Compose in the foreground:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That can work, but I usually do not prefer it for Compose stacks on servers. Detached containers plus explicit &lt;code&gt;ExecStop&lt;/code&gt; are easier to operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  A More Production-Friendly Unit
&lt;/h2&gt;

&lt;p&gt;For a real server, I prefer a slightly stricter unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;MyApp Docker Compose stack&lt;/span&gt;
&lt;span class="py"&gt;Documentation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://example.com/docs/myapp&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/myapp&lt;/span&gt;
&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-/opt/myapp/.env.systemd&lt;/span&gt;
&lt;span class="py"&gt;ExecStartPre&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose config --quiet&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecReload&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose down&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WorkingDirectory&lt;/code&gt; points to the Compose project.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExecStartPre&lt;/code&gt; validates the Compose config.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExecReload&lt;/code&gt; recreates changed services.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExecStop&lt;/code&gt; stops and removes the Compose project containers and default network.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EnvironmentFile=-...&lt;/code&gt; means the file is optional.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create the optional systemd environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /opt/myapp/.env.systemd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;COMPOSE_PROJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reload systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Compose .env vs systemd EnvironmentFile
&lt;/h2&gt;

&lt;p&gt;Compose and systemd each have their own environment mechanism, and mixing them up causes confusing "variable not set" failures at boot.&lt;/p&gt;

&lt;p&gt;Compose automatically reads a &lt;code&gt;.env&lt;/code&gt; file in the project directory for variable substitution in the Compose file.&lt;/p&gt;

&lt;p&gt;Example &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_TAG=1.2.3
WEB_PORT=8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example &lt;code&gt;compose.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:${APP_TAG}&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${WEB_PORT}:80"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A systemd &lt;code&gt;EnvironmentFile&lt;/code&gt; sets environment variables for the &lt;code&gt;docker compose&lt;/code&gt; command itself.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-/opt/myapp/.env.systemd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For many projects, you only need Compose &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use a systemd environment file when you want to define things such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;COMPOSE_PROJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;COMPOSE_FILE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;compose.yaml&lt;/span&gt;
&lt;span class="py"&gt;DOCKER_HOST&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;unix:///var/run/docker.sock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not use either file as a casual secrets vault. If secrets matter, use Docker secrets, an external secret manager, encrypted files, or at least strict permissions.&lt;/p&gt;

&lt;p&gt;Set restrictive permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /opt/myapp/.env
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /opt/myapp/.env.systemd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Restart Policies: Docker vs systemd
&lt;/h2&gt;

&lt;p&gt;There are two restart layers — container restart policy in Compose and systemd service restart policy — and they should not be mixed blindly.&lt;/p&gt;

&lt;p&gt;For long-running containers, set restart policies in Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common restart values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Do not restart automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;always&lt;/td&gt;
&lt;td&gt;Restart after exit and daemon restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;on-failure&lt;/td&gt;
&lt;td&gt;Restart only after failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unless-stopped&lt;/td&gt;
&lt;td&gt;Restart unless manually stopped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most persistent services, I prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is predictable and respects intentional manual stops.&lt;/p&gt;

&lt;p&gt;The systemd unit itself should usually not restart repeatedly, because &lt;code&gt;docker compose up -d&lt;/code&gt; is not the running workload. The containers are.&lt;/p&gt;

&lt;p&gt;So avoid this unless you have a specific reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In most Compose-as-service units, let Docker handle container restarts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health Checks
&lt;/h2&gt;

&lt;p&gt;Restart policies restart containers when processes exit. They do not magically fix every unhealthy application.&lt;/p&gt;

&lt;p&gt;Add health checks where they are useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app:latest&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-fsS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080/health&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check health:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect a container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect container-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Health checks are especially useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web apps&lt;/li&gt;
&lt;li&gt;Reverse proxies&lt;/li&gt;
&lt;li&gt;Databases&lt;/li&gt;
&lt;li&gt;Queues&lt;/li&gt;
&lt;li&gt;Internal APIs&lt;/li&gt;
&lt;li&gt;Workers with a health endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are less useful when they only check that a process exists, because a process that is alive but wedged still looks healthy. A bad health check is just another lie in YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Startup Order and depends_on
&lt;/h2&gt;

&lt;p&gt;Compose can define dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app:latest&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can help startup ordering, but do not over-trust it. Applications should still handle retries — databases restart, networks flap, DNS takes time, and a resilient app retries connections instead of assuming perfect startup order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logs: journalctl and docker compose logs
&lt;/h2&gt;

&lt;p&gt;Two log views cover most debugging: systemd captures the lifecycle of the unit itself, while Compose captures application output from running containers.&lt;/p&gt;

&lt;p&gt;systemd service logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow systemd logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose service logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose logs &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most app debugging, &lt;code&gt;docker compose logs&lt;/code&gt; is more useful; for lifecycle debugging — start failures, unit crashes, permission errors — &lt;code&gt;journalctl&lt;/code&gt; is more useful. If &lt;code&gt;systemctl start myapp&lt;/code&gt; fails, check &lt;code&gt;journalctl&lt;/code&gt; first. If the stack starts but the app is broken, check &lt;code&gt;docker compose logs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log Rotation
&lt;/h2&gt;

&lt;p&gt;Docker logs can grow forever if you do not configure them.&lt;/p&gt;

&lt;p&gt;For small servers, configure Docker log rotation in &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"log-driver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json-file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"log-opts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max-size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max-file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart the Compose stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies to newly created containers. Recreate containers if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--force-recreate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log rotation is not glamorous, but it is one of the easiest ways to prevent a disk-full outage on a small server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating a Compose Service
&lt;/h2&gt;

&lt;p&gt;A simple manual update flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If managed by systemd, you can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your unit has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ExecReload&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But note: &lt;code&gt;ExecReload&lt;/code&gt; does not pull images unless you include that step.&lt;/p&gt;

&lt;p&gt;For explicit updates, create a script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/myapp/scripts
nano /opt/myapp/scripts/update.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp

docker compose config &lt;span class="nt"&gt;--quiet&lt;/span&gt;
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /opt/myapp/scripts/update.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/opt/myapp/scripts/update.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the service unit can remain focused on lifecycle, while the update script handles deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safer Update Script with Backup Hook
&lt;/h2&gt;

&lt;p&gt;For stateful services, update only after backup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;APP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/myapp"&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/myapp/backups"&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Validating compose file"&lt;/span&gt;
docker compose config &lt;span class="nt"&gt;--quiet&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Running backup hook"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/scripts/backup.sh"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/scripts/backup.sh"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No backup hook found"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Pulling images"&lt;/span&gt;
docker compose pull

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Recreating services"&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Pruning unused images"&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Current status"&lt;/span&gt;
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is still simple, but now it encodes an operational habit: backup before change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stopping the Service
&lt;/h2&gt;

&lt;p&gt;Stop the stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, &lt;code&gt;docker compose down&lt;/code&gt; removes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Containers for services in the Compose file&lt;/li&gt;
&lt;li&gt;Networks defined by the Compose file&lt;/li&gt;
&lt;li&gt;The default network&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not remove named volumes unless you ask it to.&lt;/p&gt;

&lt;p&gt;Do not casually use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That removes named volumes declared in the Compose file and anonymous volumes attached to containers. For databases and stateful apps, that can mean deleting real data.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;down -v&lt;/code&gt; only when you mean "destroy this environment".&lt;/p&gt;

&lt;h2&gt;
  
  
  Restarting the Service
&lt;/h2&gt;

&lt;p&gt;Restart the systemd unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs the stop command and then the start command.&lt;/p&gt;

&lt;p&gt;For only restarting containers without recreating them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker compose restart&lt;/code&gt; restarts existing containers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker compose up -d&lt;/code&gt; applies config or image changes by recreating containers when needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you changed &lt;code&gt;compose.yaml&lt;/code&gt;, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling Orphan Containers
&lt;/h2&gt;

&lt;p&gt;If you rename or remove a service in &lt;code&gt;compose.yaml&lt;/code&gt;, old containers may remain as orphans.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why the systemd service examples in this guide use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It keeps the stack closer to the current Compose file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups
&lt;/h2&gt;

&lt;p&gt;Backups depend on the workload, but the principles are stable.&lt;/p&gt;

&lt;p&gt;For bind mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/myapp/data/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back up that directory.&lt;/p&gt;

&lt;p&gt;For named volumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect a volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume inspect volume-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For databases, filesystem copies are not always enough. Use application-aware backups:&lt;/p&gt;

&lt;p&gt;PostgreSQL example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; db pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; postgres appdb &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backups/appdb.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MariaDB example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; db mariadb-dump &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; appdb &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backups/appdb.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;redis redis-cli BGSAVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Compose stack without a backup plan is not a service — it is a temporary experiment that happens to have uptime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Baseline
&lt;/h2&gt;

&lt;p&gt;For a small Compose service on Linux, start with this baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the Compose project under &lt;code&gt;/opt/appname&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use explicit image tags, not only &lt;code&gt;latest&lt;/code&gt;, when stability matters.&lt;/li&gt;
&lt;li&gt;Use bind mounts or named volumes deliberately.&lt;/li&gt;
&lt;li&gt;Do not expose ports you do not need.&lt;/li&gt;
&lt;li&gt;Put public services behind a reverse proxy.&lt;/li&gt;
&lt;li&gt;Use HTTPS at the edge.&lt;/li&gt;
&lt;li&gt;Keep secrets out of Git.&lt;/li&gt;
&lt;li&gt;Restrict &lt;code&gt;.env&lt;/code&gt; permissions.&lt;/li&gt;
&lt;li&gt;Avoid privileged containers unless truly required.&lt;/li&gt;
&lt;li&gt;Avoid mounting the Docker socket into containers.&lt;/li&gt;
&lt;li&gt;Keep Docker and images updated.&lt;/li&gt;
&lt;li&gt;Test firewall behavior from another machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A dangerous pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the container control over Docker. In practice, that can become host-level control. Use it only when you understand the risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource Limits
&lt;/h2&gt;

&lt;p&gt;On small servers, one bad container can consume the host.&lt;/p&gt;

&lt;p&gt;Compose supports resource-related settings, but behavior can depend on Docker Engine and Compose version. For simple protection, start with application-level limits and Docker logging limits.&lt;/p&gt;

&lt;p&gt;For some workloads, you can add memory limits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;mem_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also configure app-level worker counts, queue limits, and cache sizes. Container limits are useful, but they are not a substitute for understanding the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: A Realistic Compose Service
&lt;/h2&gt;

&lt;p&gt;Directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/whoami/
  compose.yaml
  .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whoami&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik/whoami:v1.10&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${WHOAMI_PORT}:80"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wget&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-qO-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WHOAMI_PORT=8080
COMPOSE_PROJECT_NAME=whoami
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;systemd unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Whoami Docker Compose stack&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/whoami&lt;/span&gt;
&lt;span class="py"&gt;ExecStartPre&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose config --quiet&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecReload&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose down&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; whoami.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status whoami.service
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/whoami
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Service Starts but Containers Are Not Running
&lt;/h3&gt;

&lt;p&gt;Check systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status docker
docker info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  WorkingDirectory Is Wrong
&lt;/h3&gt;

&lt;p&gt;If systemd cannot find your Compose file, confirm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/myapp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /opt/myapp
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /opt/myapp/compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service runs from &lt;code&gt;WorkingDirectory&lt;/code&gt;, not from your current shell directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Permission Denied
&lt;/h3&gt;

&lt;p&gt;If the unit runs as root, it can normally access Docker.&lt;/p&gt;

&lt;p&gt;If you set &lt;code&gt;User=someuser&lt;/code&gt;, that user must be able to access Docker. Usually that means membership in the &lt;code&gt;docker&lt;/code&gt; group, or a rootless Docker setup.&lt;/p&gt;

&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;groups &lt;/span&gt;someuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the user if appropriate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker someuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be careful. The Docker group is effectively privileged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compose Command Not Found
&lt;/h3&gt;

&lt;p&gt;Find Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the full path in the unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Compose plugin is missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install it using your Docker package source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment Variables Are Missing
&lt;/h3&gt;

&lt;p&gt;Check the Compose config as systemd would see it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If systemd needs extra environment variables, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-/opt/myapp/.env.systemd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Compose needs variables for substitution, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/myapp/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are related, but not identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Containers Do Not Start After Reboot
&lt;/h3&gt;

&lt;p&gt;Check whether the systemd service is enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl is-enabled myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl is-enabled docker
systemctl status docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check boot logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  App Starts Before Database Is Ready
&lt;/h3&gt;

&lt;p&gt;Add a database health check and &lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;service_healthy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also fix the application. It should retry database connections. Infrastructure startup ordering is helpful, but application retry logic is better.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disk Filled with Docker Logs
&lt;/h3&gt;

&lt;p&gt;Check Docker disk usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker system &lt;span class="nb"&gt;df&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check large container logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo du&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /var/lib/docker/containers | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure Docker log rotation in &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then recreate containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Running docker compose up in rc.local
&lt;/h3&gt;

&lt;p&gt;Running &lt;code&gt;docker compose up&lt;/code&gt; from &lt;code&gt;rc.local&lt;/code&gt; or a login script works until it does not — use a proper systemd unit instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: Using Restart=always in systemd and restart: always in Compose
&lt;/h3&gt;

&lt;p&gt;Usually you only need container restart policies in Compose. Avoid two supervisors fighting each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Forgetting --remove-orphans
&lt;/h3&gt;

&lt;p&gt;Service renames and removals can leave old containers behind. Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake 4: Using docker compose restart After Config Changes
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;restart&lt;/code&gt; restarts containers. It does not apply all configuration changes.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake 5: Running down -v Without Thinking
&lt;/h3&gt;

&lt;p&gt;This can delete volumes. For stateful services, that can mean deleting data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 6: No Backup Before Pull
&lt;/h3&gt;

&lt;p&gt;New images can break. Databases can migrate. Tags can move. Back up first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 7: Publishing Every Port
&lt;/h3&gt;

&lt;p&gt;Only publish what the host needs to expose. Internal service-to-service traffic can stay on the Compose network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Recommended Pattern
&lt;/h2&gt;

&lt;p&gt;For most single-host Linux services, use this pattern:&lt;/p&gt;

&lt;p&gt;Compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;systemd unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;MyApp Docker Compose stack&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/myapp&lt;/span&gt;
&lt;span class="py"&gt;ExecStartPre&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose config --quiet&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecReload&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose down&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Operate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status myapp.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart myapp.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is not fancy, and that is the point. Docker Compose is excellent for small, understandable systems, systemd is excellent at starting and stopping host services, and together they give you a reliable single-server deployment model without pretending every project needs a cluster. For container-level commands outside Compose — images, volumes, networks, and cleanup — see the &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-cheatsheet/" rel="noopener noreferrer"&gt;Docker Cheatsheet&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>selfhosting</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
