<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ self-hosted - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ self-hosted - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 12 Jun 2026 23:04:45 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/self-hosted/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Self-Hosted WhatsApp Bot with n8n and WAHA ]]>
                </title>
                <description>
                    <![CDATA[ WhatsApp is where your many of your customers likely already are. For support tickets, order updates, booking reminders, and lead qualification, a WhatsApp channel often converts several times better  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-self-hosted-whatsapp-bot-with-n8n-and-waha/</link>
                <guid isPermaLink="false">6a01e032fca21b0d4b2bb4c1</guid>
                
                    <category>
                        <![CDATA[ whatsapp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ automation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ n8n ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ self-hosted ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ אחיה כהן ]]>
                </dc:creator>
                <pubDate>Mon, 11 May 2026 13:57:06 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/28affe4d-9359-4cbb-a311-a2ee9d0829c0.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>WhatsApp is where your many of your customers likely already are. For support tickets, order updates, booking reminders, and lead qualification, a WhatsApp channel often converts several times better than email.</p>
<p>But the official WhatsApp Business Cloud API can be slow to onboard, template-restricted for proactive messages, and priced per conversation — which adds up fast at scale.</p>
<p>There's another path: you can run your own WhatsApp HTTP gateway on a small server, connect it to a workflow engine, and keep every message — inbound and outbound — inside infrastructure you control. No monthly conversation fees, no template approvals for routine replies, no third-party middleman holding your customer data.</p>
<p>In this tutorial, you'll build exactly that. By the end, you'll have a WhatsApp bot that:</p>
<ul>
<li><p>Receives every incoming message through a webhook</p>
</li>
<li><p>Routes messages through an n8n workflow</p>
</li>
<li><p>Replies automatically based on keywords, AI, or any API call you want</p>
</li>
<li><p>Runs entirely on your own server, using two open-source tools</p>
</li>
</ul>
<p>You'll use <strong>WAHA</strong> (WhatsApp HTTP API) as the gateway, and <strong>n8n</strong> as the workflow engine. Both run in Docker, both are free for self-hosting, and together they cover everything from a simple auto-reply to a full CRM integration.</p>
<h2 id="heading-table-of-contents">Table of contents</h2>
<ul>
<li><p><a href="#heading-what-youll-learn">What You'll Learn</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-a-note-on-which-whatsapp-account-to-use">A Note on Which WhatsApp Account to Use</a></p>
</li>
<li><p><a href="#heading-waha-vs-the-official-whatsapp-business-cloud-api">WAHA vs the official WhatsApp Business Cloud API</a></p>
</li>
<li><p><a href="#heading-part-1-understanding-waha">Part 1: Understanding WAHA</a></p>
</li>
<li><p><a href="#heading-part-2-running-waha-with-docker">Part 2: Running WAHA with Docker</a></p>
</li>
<li><p><a href="#heading-part-3-starting-a-whatsapp-session">Part 3: Starting a WhatsApp session</a></p>
</li>
<li><p><a href="#heading-part-4-running-n8n">Part 4: Running n8n</a></p>
</li>
<li><p><a href="#heading-part-5-creating-the-webhook-trigger-in-n8n">Part 5: Creating the Webhook Trigger in n8n</a></p>
</li>
<li><p><a href="#heading-part-6-wiring-waha-to-n8n">Part 6: Wiring WAHA to n8n</a></p>
</li>
<li><p><a href="#heading-part-7-building-the-first-auto-reply">Part 7: Building the first auto-reply</a></p>
</li>
<li><p><a href="#heading-part-8-a-second-example-proactive-booking-confirmations">Part 8: A Second Example — Proactive Booking Confirmations</a></p>
</li>
<li><p><a href="#heading-part-9-going-to-production">Part 9: Going to Production</a></p>
</li>
<li><p><a href="#heading-common-pitfalls">Common Pitfalls</a></p>
</li>
<li><p><a href="#heading-where-to-go-next">Where to Go Next</a></p>
</li>
</ul>
<h2 id="heading-what-youll-learn">What You'll Learn</h2>
<ul>
<li><p>How WAHA works under the hood and when to use it instead of the official Cloud API</p>
</li>
<li><p>How to run WAHA and n8n side by side with Docker Compose</p>
</li>
<li><p>How to scan the QR code and bind a WhatsApp account to your gateway</p>
</li>
<li><p>How to connect WAHA's webhook to an n8n workflow</p>
</li>
<li><p>How to build a keyword-based auto-reply bot</p>
</li>
<li><p>How to send proactive confirmations from a separate workflow</p>
</li>
<li><p>How to harden the setup for production (HTTPS, API keys, rate limits, Queue Mode)</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>A Linux server (any VPS works — 2 GB of RAM is enough for a small bot)</p>
</li>
<li><p>Docker and Docker Compose installed</p>
</li>
<li><p>A public hostname with DNS pointing at the server, or an ngrok tunnel for local testing</p>
</li>
<li><p>A WhatsApp account you're willing to dedicate to the bot (more on that below)</p>
</li>
<li><p>Basic familiarity with JSON and HTTP requests</p>
</li>
</ul>
<p>You don't need prior n8n experience. If you can drag a box and wire it to another box, you can build the flow.</p>
<h2 id="heading-a-note-on-which-whatsapp-account-to-use">A Note on Which WhatsApp Account to Use</h2>
<p>WAHA works by running an actual WhatsApp Web session inside a headless Chromium process. It logs in as a real account — the same way you would open web.whatsapp.com in your browser. Meta doesn't officially endorse this approach for commercial use at scale, and heavy volume from a single number can lead to a ban.</p>
<p>For that reason, use a dedicated number for the bot. Don't use your personal WhatsApp. Get a second SIM, eSIM, or a VoIP number that supports WhatsApp activation. Keep outbound volume reasonable, and you'll be fine for most small-business use cases.</p>
<p>If you plan to send thousands of marketing messages per day, switch to the official WhatsApp Business Cloud API — that's what it exists for. This tutorial is aimed at the middle ground: support bots, order updates, booking confirmations, and similar conversational flows where you need real-time control without enterprise pricing.</p>
<h2 id="heading-waha-vs-the-official-whatsapp-business-cloud-api">WAHA vs the official WhatsApp Business Cloud API</h2>
<p>Before writing any code, it helps to understand when each option is the right fit.</p>
<table>
<thead>
<tr>
<th>Dimension</th>
<th>WAHA (self-hosted)</th>
<th>WhatsApp Cloud API (Meta)</th>
</tr>
</thead>
<tbody><tr>
<td>Onboarding</td>
<td>Scan a QR code — ready in minutes</td>
<td>Business verification, app review — days to weeks</td>
</tr>
<tr>
<td>Cost</td>
<td>Server cost only</td>
<td>Per-conversation pricing</td>
</tr>
<tr>
<td>Template approval</td>
<td>Not needed</td>
<td>Required for proactive messages outside the 24-hour window</td>
</tr>
<tr>
<td>Session model</td>
<td>One WhatsApp Web session per Core container</td>
<td>Native API, no web session</td>
</tr>
<tr>
<td>Risk</td>
<td>Account ban possible at high unsolicited volume</td>
<td>Rate limits but no ban for normal use</td>
</tr>
<tr>
<td>Vendor lock-in</td>
<td>None — pure open source</td>
<td>Tied to Meta's API and pricing</td>
</tr>
<tr>
<td>Best for</td>
<td>Support bots, small-team workflows, internal tools</td>
<td>High-volume marketing, regulated industries, &gt;100k monthly messages</td>
</tr>
</tbody></table>
<p>Neither is strictly better. If you run a support team for a small business, WAHA is often the pragmatic choice. If you're a bank sending millions of transactional messages, you want the Cloud API. Many teams run both — WAHA for conversational support, Cloud API for bulk transactional traffic.</p>
<h2 id="heading-part-1-understanding-waha">Part 1: Understanding WAHA</h2>
<p>WAHA is an open-source project that wraps WhatsApp Web behind a clean REST API. You <code>POST /api/sendText</code> with a chat ID and a message, and WAHA sends it. You configure a webhook URL, and WAHA <code>POST</code>s to that URL every time a message arrives.</p>
<p>Under the hood, WAHA spawns a Chromium instance, opens WhatsApp Web, and uses an engine (<code>whatsapp-web.js</code>, <code>NOWEB</code>, or <code>GOWS</code>) to automate the session. Your code doesn't see any of that complexity — you just see an HTTP API.</p>
<p>The project ships in two flavors:</p>
<ul>
<li><p><strong>WAHA Core</strong> — free, MIT licensed, one active session per container, community support.</p>
</li>
<li><p><strong>WAHA Plus</strong> — commercial license, multi-session support, priority support, and access to advanced endpoints.</p>
</li>
</ul>
<p>For most developers building a single bot, Core is enough. You can always upgrade later.</p>
<p>Official docs live at <a href="https://waha.devlike.pro/">waha.devlike.pro</a>. Keep that open in another tab — we'll reference specific endpoints as we go.</p>
<h2 id="heading-part-2-running-waha-with-docker">Part 2: Running WAHA with Docker</h2>
<p>Create a fresh directory for the project:</p>
<pre><code class="language-bash">mkdir whatsapp-bot &amp;&amp; cd whatsapp-bot
</code></pre>
<p>Create a <code>docker-compose.yml</code> file:</p>
<pre><code class="language-yaml">services:
  waha:
    image: devlikeapro/waha:latest
    container_name: waha
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - WAHA_DASHBOARD_ENABLED=true
      - WAHA_DASHBOARD_USERNAME=admin
      - WAHA_DASHBOARD_PASSWORD=change-me-now
      - WHATSAPP_API_KEY=super-secret-key-change-me
      - WHATSAPP_DEFAULT_ENGINE=WEBJS
    volumes:
      - ./waha-sessions:/app/.sessions
</code></pre>
<p>A few things to notice:</p>
<ul>
<li><p>The dashboard username and password protect the web UI at <code>http://your-server:3000</code>. Always change the defaults before you expose the port publicly.</p>
</li>
<li><p><code>WHATSAPP_API_KEY</code> is the key every HTTP request to WAHA must include in the <code>X-Api-Key</code> header. Treat it like a database password.</p>
</li>
<li><p><code>WHATSAPP_DEFAULT_ENGINE=WEBJS</code> uses the mature <code>whatsapp-web.js</code> engine. WAHA also supports <code>NOWEB</code> and <code>GOWS</code> engines with different trade-offs — WEBJS is the safest default for a first deployment.</p>
</li>
<li><p>The volume mount persists the session across restarts. Without it, every container rebuild forces you to scan the QR code again.</p>
</li>
</ul>
<p>Start the container:</p>
<pre><code class="language-bash">docker compose up -d
docker compose logs -f waha
</code></pre>
<p>Within about 20 seconds WAHA finishes booting. Visit <code>http://your-server:3000</code> and log in with the dashboard credentials.</p>
<h2 id="heading-part-3-starting-a-whatsapp-session">Part 3: Starting a WhatsApp session</h2>
<p>WAHA calls each WhatsApp account a "session." You can have one session at a time on WAHA Core.</p>
<p>From the dashboard, click <strong>Start New Session</strong> and name it <code>default</code>. WAHA displays a QR code.</p>
<p>On your phone:</p>
<ol>
<li><p>Open WhatsApp.</p>
</li>
<li><p>Tap the three-dot menu (Android) or Settings (iOS).</p>
</li>
<li><p>Tap Linked Devices → Link a Device.</p>
</li>
<li><p>Point the camera at the QR code on your screen.</p>
</li>
</ol>
<p>Within a few seconds the dashboard shows <code>WORKING</code> status. Your session is live.</p>
<p>You can also do this over the API. Start the session (<code>default</code> is the session name, encoded in the URL path):</p>
<pre><code class="language-bash">curl -X POST http://your-server:3000/api/sessions/default/start \
  -H "X-Api-Key: super-secret-key-change-me"
</code></pre>
<p>The call is idempotent — if the session is already running, nothing happens.</p>
<p>Fetch the QR as a PNG:</p>
<pre><code class="language-bash">curl http://your-server:3000/api/default/auth/qr \
  -H "X-Api-Key: super-secret-key-change-me" \
  -H "Accept: image/png" \
  --output qr.png
</code></pre>
<p>Scan and you're in.</p>
<p>Test that the session works by sending a message to yourself:</p>
<pre><code class="language-bash">curl -X POST http://your-server:3000/api/sendText \
  -H "X-Api-Key: super-secret-key-change-me" \
  -H "Content-Type: application/json" \
  -d '{
    "session": "default",
    "chatId": "15555550123@c.us",
    "text": "Hello from WAHA!"
  }'
</code></pre>
<p>Replace <code>15555550123</code> with your own number (country code plus number, no <code>+</code>, no spaces, no dashes). The <code>@c.us</code> suffix marks it as an individual chat. Groups use <code>@g.us</code>.</p>
<p>If the message lands on your phone — congratulations. The gateway works.</p>
<h2 id="heading-part-4-running-n8n">Part 4: Running n8n</h2>
<p>Add an <code>n8n</code> service to your <code>docker-compose.yml</code> alongside WAHA:</p>
<pre><code class="language-yaml">services:
  waha:
    # ... existing config

  n8n:
    image: n8nio/n8n:latest
    container_name: n8n
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      - N8N_HOST=n8n.example.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://n8n.example.com/
      - GENERIC_TIMEZONE=UTC
    volumes:
      - ./n8n-data:/home/node/.n8n
</code></pre>
<p>Replace <code>n8n.example.com</code> with your real domain. For purely local testing, set:</p>
<pre><code class="language-yaml">- N8N_HOST=localhost
- N8N_PROTOCOL=http
- WEBHOOK_URL=http://localhost:5678/
</code></pre>
<p>If you want to test webhooks from your laptop without a server, run <code>ngrok http 5678</code> in another terminal and use the ngrok HTTPS URL as <code>WEBHOOK_URL</code>. n8n uses <code>WEBHOOK_URL</code> to tell external services where to POST — get this wrong and your webhooks will 404.</p>
<p>Start the stack:</p>
<pre><code class="language-bash">docker compose up -d
</code></pre>
<p>Visit <code>http://your-server:5678</code>. On the first visit, n8n walks you through creating an owner account (email and password). Every subsequent visit requires that login. For extra safety in production, put n8n behind a reverse proxy with an allow-list or an additional auth layer — we'll set that up later.</p>
<h2 id="heading-part-5-creating-the-webhook-trigger-in-n8n">Part 5: Creating the Webhook Trigger in n8n</h2>
<p>Click Create Workflow. You'll see an empty canvas.</p>
<p>Add a Webhook node and configure it:</p>
<ul>
<li><p><strong>HTTP Method</strong>: POST</p>
</li>
<li><p><strong>Path</strong>: <code>whatsapp</code> (this becomes part of the URL)</p>
</li>
<li><p><strong>Response Mode</strong>: Respond Immediately</p>
</li>
<li><p><strong>Response Data</strong>: First Entry JSON</p>
</li>
</ul>
<p>Click Listen for Test Event. n8n shows you two URLs: a test URL and a production URL. Copy the production URL. It looks like this:</p>
<pre><code class="language-plaintext">https://n8n.example.com/webhook/whatsapp
</code></pre>
<p>Not <code>webhook-test</code> — that one only fires while the editor is open. You want <code>webhook</code>.</p>
<h2 id="heading-part-6-wiring-waha-to-n8n">Part 6: Wiring WAHA to n8n</h2>
<p>WAHA can POST to a webhook on every WhatsApp event. Tell it where to send those events.</p>
<p>In the WAHA dashboard, open your session and set the webhook URL. Or do it over the API:</p>
<pre><code class="language-bash">curl -X PUT http://your-server:3000/api/sessions/default \
  -H "X-Api-Key: super-secret-key-change-me" \
  -H "Content-Type: application/json" \
  -d '{
    "config": {
      "webhooks": [
        {
          "url": "https://n8n.example.com/webhook/whatsapp",
          "events": ["message", "session.status"]
        }
      ]
    }
  }'
</code></pre>
<p>The <code>message</code> event fires on every inbound message. <code>session.status</code> fires when the session connects, disconnects, or reconnects — which is useful for alerting when your bot goes down.</p>
<p>Test it. From another phone, send a WhatsApp message to your bot's number. Head back to the n8n editor. Within a second or two the webhook node lights up with the event data.</p>
<p>The payload looks roughly like this:</p>
<pre><code class="language-json">{
  "event": "message",
  "session": "default",
  "payload": {
    "id": "false_15555550123@c.us_3EB0...",
    "from": "15555550123@c.us",
    "body": "Hello",
    "timestamp": 1713801234,
    "fromMe": false
  }
}
</code></pre>
<p>Everything you need is in <code>payload</code>: who sent it (<code>from</code>), what they said (<code>body</code>), and when (<code>timestamp</code>).</p>
<h2 id="heading-part-7-building-the-first-auto-reply">Part 7: Building the first auto-reply</h2>
<p>A bot that only listens is boring. Let's make it answer.</p>
<p>You'll build a tiny keyword router: if the user sends <code>hi</code> or <code>hello</code>, the bot greets them. If they send <code>price</code>, it sends a pricing message. Anything else gets a fallback.</p>
<p>After the Webhook node, add a Switch node.</p>
<p>Configure the Switch node:</p>
<ul>
<li><p><strong>Mode</strong>: Expression</p>
</li>
<li><p><strong>Value</strong>: <code>{{ $json.payload.body.toLowerCase().trim() }}</code></p>
</li>
<li><p>Add routing rules:</p>
<ul>
<li><p>Rule 1: equals <code>hi</code> — output 0</p>
</li>
<li><p>Rule 2: equals <code>hello</code> — output 0</p>
</li>
<li><p>Rule 3: equals <code>price</code> — output 1</p>
</li>
<li><p>Fallback output: 2</p>
</li>
</ul>
</li>
</ul>
<p>After the Switch, add three HTTP Request nodes, one per output.</p>
<p>Configure each HTTP Request node identically, except for the body text:</p>
<ul>
<li><p><strong>Method</strong>: POST</p>
</li>
<li><p><strong>URL</strong>: <code>http://waha:3000/api/sendText</code> (inside the Docker network you can reach WAHA by its service name. From outside use the full public URL)</p>
</li>
<li><p><strong>Send Headers</strong>: on</p>
<ul>
<li><p><code>X-Api-Key</code>: <code>super-secret-key-change-me</code></p>
</li>
<li><p><code>Content-Type</code>: <code>application/json</code></p>
</li>
</ul>
</li>
<li><p><strong>Send Body</strong>: on</p>
<ul>
<li><p><strong>Body Content Type</strong>: JSON</p>
</li>
<li><p><strong>Specify Body</strong>: Using JSON</p>
</li>
</ul>
</li>
</ul>
<p>For the greeting node, the JSON body is:</p>
<pre><code class="language-json">{
  "session": "default",
  "chatId": "={{ $('Webhook').item.json.payload.from }}",
  "text": "Hi! I'm the bot. Send 'price' to see pricing, or anything else for help."
}
</code></pre>
<p>For the pricing node:</p>
<pre><code class="language-json">{
  "session": "default",
  "chatId": "={{ $('Webhook').item.json.payload.from }}",
  "text": "Our plans start at $49/month. Reply 'sales' to talk to a human."
}
</code></pre>
<p>For the fallback:</p>
<pre><code class="language-json">{
  "session": "default",
  "chatId": "={{ $('Webhook').item.json.payload.from }}",
  "text": "I didn't catch that. Try 'hi' or 'price'."
}
</code></pre>
<p>The <code>={{ ... }}</code> syntax is an n8n expression — at runtime it pulls values from earlier nodes.</p>
<p>Connect the Switch outputs to their matching HTTP Request nodes. Save the workflow. Click Activate in the top-right.</p>
<p>Send <code>hi</code> to your bot from any phone. It should reply within a second.</p>
<p>Congratulations — you have a WhatsApp bot running entirely on your own infrastructure.</p>
<h2 id="heading-part-8-a-second-example-proactive-booking-confirmations">Part 8: A Second Example — Proactive Booking Confirmations</h2>
<p>Auto-reply is useful. Proactive outbound is where the value really compounds. Here's a second workflow that sends a booking confirmation whenever a new row lands in a database.</p>
<p>Create a second workflow in n8n. Use one of these triggers:</p>
<ul>
<li><p><strong>Schedule Trigger</strong> — poll a database every minute for new rows</p>
</li>
<li><p><strong>Webhook Trigger</strong> — listen for a notification from your booking system</p>
</li>
<li><p><strong>Database Trigger</strong> (Postgres, MySQL, Supabase) — react to inserts in real time</p>
</li>
</ul>
<p>For this example, use a Schedule Trigger set to every minute, followed by a Postgres <strong>Execute Query</strong> node that reads pending confirmations:</p>
<pre><code class="language-sql">SELECT id, customer_phone, service_name, booking_time
FROM bookings
WHERE confirmation_sent = false
LIMIT 20;
</code></pre>
<p>After the Postgres node, add an HTTP Request node pointing to the same WAHA <code>sendText</code> endpoint you used earlier. The body:</p>
<pre><code class="language-json">{
  "session": "default",
  "chatId": "={{ $json.customer_phone }}@c.us",
  "text": "Hi! Your booking for {{ \(json.service_name }} on {{ \)json.booking_time }} is confirmed. Reply 'change' to reschedule."
}
</code></pre>
<p>Finally, add a second Postgres node that marks the booking as sent:</p>
<pre><code class="language-sql">UPDATE bookings
SET confirmation_sent = true, confirmation_sent_at = NOW()
WHERE id = {{ $json.id }};
</code></pre>
<p>Activate the workflow. Every minute, n8n pulls pending bookings, sends a WhatsApp confirmation, and marks them done.</p>
<p>This pattern generalizes. Replace the SQL with a call to Shopify for order confirmations, Stripe for receipt messages, or Calendly for appointment reminders. The WhatsApp layer stays the same — only the source of truth changes.</p>
<h2 id="heading-part-9-going-to-production">Part 9: Going to Production</h2>
<p>The setup above works, but it's not yet production-ready. Here's what to harden before you point real customers at it.</p>
<h3 id="heading-1-put-everything-behind-https">1. Put Everything Behind HTTPS</h3>
<p>Never expose n8n or WAHA directly on plain HTTP. Put a reverse proxy in front. Caddy is the easiest choice because it handles Let's Encrypt automatically.</p>
<p>A minimal <code>Caddyfile</code>:</p>
<pre><code class="language-plaintext">n8n.example.com {
    reverse_proxy n8n:5678
}

waha.example.com {
    reverse_proxy waha:3000
}
</code></pre>
<p>Run Caddy as another service in the same Docker Compose. TLS certificates are issued and renewed automatically.</p>
<h3 id="heading-2-rotate-the-api-keys">2. Rotate the API Keys</h3>
<p>Don't ship <code>super-secret-key-change-me</code> to production. Generate a real key:</p>
<pre><code class="language-bash">openssl rand -hex 32
</code></pre>
<p>Put it in a <code>.env</code> file, reference it as <code>${WHATSAPP_API_KEY}</code> in <code>docker-compose.yml</code>, and add <code>.env</code> to your <code>.gitignore</code>.</p>
<h3 id="heading-3-rate-limit-outbound-messages">3. Rate-limit Outbound Messages</h3>
<p>WhatsApp bans accounts that send too many messages too fast. A safe outbound rate for a fresh number is well under 20 messages per minute. For bursty replies, add an n8n Wait node between sends, or queue outgoing messages through a small custom function node that sleeps between requests.</p>
<h3 id="heading-4-scale-n8n-with-queue-mode">4. Scale n8n with Queue Mode</h3>
<p>By default, n8n runs everything in a single process. That's fine for low volume. For higher throughput, switch to Queue Mode:</p>
<ul>
<li><p>Add a Redis container.</p>
</li>
<li><p>Run one <code>n8n</code> main container (the web UI and webhook receiver).</p>
</li>
<li><p>Run one or more <code>n8n-worker</code> containers that pull jobs from the queue.</p>
</li>
</ul>
<p>Queue Mode is documented at <a href="https://docs.n8n.io/hosting/scaling/queue-mode/">docs.n8n.io/hosting/scaling/queue-mode/</a>. Setup adds two environment variables (<code>EXECUTIONS_MODE=queue</code>, <code>QUEUE_BULL_REDIS_HOST=redis</code>) and decouples incoming webhooks from workflow execution. The webhook responds in milliseconds while workers chew through the queue in the background.</p>
<h3 id="heading-5-monitor-the-session">5. Monitor the Session</h3>
<p>WhatsApp Web sessions drop. The phone loses connection, WhatsApp rotates security tokens, or your server reboots. Catch those drops early.</p>
<p>Subscribe to the <code>session.status</code> webhook event in WAHA. When status becomes <code>FAILED</code> or <code>STOPPED</code>, route it to an n8n workflow that posts to Slack, sends an email, or pages you. The faster you know, the faster you recover.</p>
<p>For overall uptime, point something like Uptime Kuma at <code>GET /api/sessions/default</code> on WAHA. If WAHA reports <code>WORKING</code>, you're fine. Anything else triggers an alert.</p>
<h3 id="heading-6-back-up-the-sessions-volume">6. Back Up the Sessions Volume</h3>
<p>The <code>waha-sessions</code> directory contains the logged-in state. If you lose it, you have to scan the QR code again — possibly from a phone that's no longer handy. Back it up nightly. A simple cron job with <code>tar</code> and <code>rclone</code> to S3-compatible storage is plenty.</p>
<h3 id="heading-7-add-a-live-agent-handoff">7. Add a Live-Agent Handoff</h3>
<p>Not every conversation should stay with the bot. When a user types <code>human</code> — or when your intent classifier can't answer confidently — hand off to a real agent.</p>
<p>Chatwoot is a solid open-source option: it has a dedicated WhatsApp channel, agent inbox, team assignment, and conversation history. The handoff is an n8n branch that stops processing bot replies and forwards the message stream to Chatwoot's API.</p>
<h2 id="heading-common-pitfalls">Common Pitfalls</h2>
<p>A few issues catch almost everyone on their first production deploy.</p>
<h3 id="heading-webhooks-timing-out">Webhooks Timing Out</h3>
<p>WAHA gives your webhook a few seconds to respond. If your n8n workflow is slow (calling an LLM, hitting a remote API), the webhook times out and WAHA retries, potentially causing duplicate replies.</p>
<p>Fix: make the webhook return <code>200</code> immediately and offload the slow work. In n8n, set the Webhook node's Response Mode to <em>Using Respond to Webhook Node</em>, add a Respond to Webhook node as the first step with a <code>200</code> and empty body, then do the heavy lifting after that.</p>
<h3 id="heading-duplicate-messages">Duplicate Messages</h3>
<p>WAHA delivers the same <code>message</code> event more than once in edge cases (phone comes back online, session reconnects). Store the <code>payload.id</code> somewhere — Redis, a database, or n8n's static data store — and drop any ID you've already processed.</p>
<h3 id="heading-messages-arriving-out-of-order">Messages Arriving Out of Order</h3>
<p>The webhook is async, and n8n may parallelize executions. If ordering matters — for example, in a multi-step conversation — key a queue by the sender's <code>chatId</code> and process each sender serially.</p>
<h3 id="heading-sessions-disconnecting-after-a-phone-restart">Sessions Disconnecting After a Phone Restart</h3>
<p>Normal WhatsApp Web behavior. WAHA auto-reconnects, but occasionally the linked-devices list needs a manual refresh. If a session refuses to come back, stop the WAHA container, delete that session's folder under <code>waha-sessions/</code>, start the container again, and rescan the QR.</p>
<h3 id="heading-your-number-gets-banned">Your Number Gets Banned</h3>
<p>The single biggest cause is rate: a new number blasting hundreds of messages an hour gets flagged fast. Warm up a number slowly — send a normal, human-like volume for the first week. Don't send to strangers unsolicited. Prefer inbound-driven replies over outbound pushes wherever you can.</p>
<h3 id="heading-the-wrong-chat-id-format">The Wrong Chat ID Format</h3>
<p>WhatsApp individual chats use <code>&lt;number&gt;@c.us</code> and groups use <code>&lt;groupId&gt;@g.us</code>. Don't include the <code>+</code> or spaces in the number. If WAHA returns a 404 when sending, the chat ID is almost always the problem.</p>
<h2 id="heading-where-to-go-next">Where to Go Next</h2>
<p>You now have the foundation. The same two-service stack supports almost any bot you can imagine — you're only limited by what you can build in an n8n workflow.</p>
<p>Some natural next steps:</p>
<ul>
<li><p><strong>Plug in AI replies:</strong> Add an OpenAI or Anthropic node after the Webhook, pass the user's message through it with a short system prompt, and send the response back through WAHA. Cap conversation length to prevent runaway token usage.</p>
</li>
<li><p><strong>Integrate a CRM:</strong> Look up the caller's <code>chatId</code> in HubSpot, Pipedrive, or your own database before deciding how to reply. Segment responses by customer tier.</p>
</li>
<li><p><strong>Send proactive notifications:</strong> Appointment reminders, shipping updates, payment receipts, abandoned-cart nudges. Keep the content transactional and expected — unsolicited marketing blasts are the fastest way to a ban.</p>
</li>
<li><p><strong>Log every conversation:</strong> Add a Postgres or Supabase node after the Webhook to persist messages for analytics and customer history. Your future self (and your support team) will thank you.</p>
</li>
<li><p><strong>Add media handling:</strong> WAHA exposes <code>sendImage</code>, <code>sendFile</code>, and <code>sendVoice</code> endpoints. Teach the bot to accept photos for support tickets, or send invoices as PDFs directly inside the chat.</p>
</li>
</ul>
<p>The WhatsApp layer stays the same. Everything interesting happens upstream in the workflow.</p>
<p><em>If you want to see production examples of n8n and WAHA running at scale — or you need a similar automation built for your business — I'm the founder of Achiya Automation, where we ship WhatsApp, n8n, and Chatwoot integrations. You can find more at</em> <a href="https://achiya-automation.com"><em>achiya-automation.com</em></a><em>.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Self-Host Your Own Server Monitoring Dashboard Using Uptime Kuma and Docker ]]>
                </title>
                <description>
                    <![CDATA[ As a developer, there's nothing worse than finding out from an angry user that your website is down. Usually, you don't know your server crashed until someone complains. And while many SaaS tools can  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/self-host-uptime-kuma-docker/</link>
                <guid isPermaLink="false">69d4185f40c9cabf44851652</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ self-hosted ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ monitoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Ubuntu ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Abdul Talha ]]>
                </dc:creator>
                <pubDate>Mon, 06 Apr 2026 20:32:31 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ea068a20-bc19-400a-a42e-1bbb7e492da8.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As a developer, there's nothing worse than finding out from an angry user that your website is down. Usually, you don't know your server crashed until someone complains.</p>
<p>And while many SaaS tools can monitor your site, they often charge high monthly fees for simple alerts.</p>
<p>My goal with this article is to help you stop paying those expensive fees by showing you a powerful, free, open-source alternative called Uptime Kuma.</p>
<p>In this guide, you'll learn how to use Docker to deploy Uptime Kuma safely on a local Ubuntu machine.</p>
<p>By the end of this tutorial, you'll have set up your own private server monitoring dashboard in less than 10 minutes and created an automated Discord alert to ping your phone if your website goes offline.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-step-1-update-packages-and-prepare-the-firewall">Step 1: Update Packages and Prepare the Firewall</a></p>
</li>
<li><p><a href="#heading-step-2-create-the-docker-compose-file">Step 2: Create the Docker Compose File</a></p>
</li>
<li><p><a href="#heading-step-3-start-the-application">Step 3: Start the Application</a></p>
</li>
<li><p><a href="#heading-step-4-access-the-dashboard">Step 4: Access the Dashboard</a></p>
</li>
<li><p><a href="#heading-step-5-use-case-monitor-a-website-and-send-discord-alerts">Step 5: Use Case – Monitor a Website and Send Discord Alerts</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you start, make sure you have:</p>
<ul>
<li><p>An Ubuntu machine (like a local server, VM, or desktop).</p>
</li>
<li><p>Docker and Docker Compose installed.</p>
</li>
<li><p>Basic knowledge of the Linux terminal.</p>
</li>
</ul>
<h2 id="heading-step-1-update-packages-and-prepare-the-firewall">Step 1: Update Packages and Prepare the Firewall</h2>
<p>First, you'll want to make sure your system has the newest updates. Then, you'll install the Uncomplicated Firewall (UFW) and open the network "door" (port) that Uptime Kuma uses for the dashboard. You'll also need to allow SSH so you don't lock yourself out.</p>
<p>Run these commands in your terminal:</p>
<ol>
<li>Update your packages:</li>
</ol>
<pre><code class="language-shell">sudo apt update &amp;&amp; sudo apt upgrade -y
</code></pre>
<ol>
<li>Install the firewall:</li>
</ol>
<pre><code class="language-shell">sudo apt install ufw -y
</code></pre>
<ol>
<li>Allow SSH and open port 3001:</li>
</ol>
<pre><code class="language-shell">sudo ufw allow ssh
sudo ufw allow 3001/tcp
</code></pre>
<ol>
<li>Enable the firewall:</li>
</ol>
<pre><code class="language-shell">sudo ufw enable
sudo ufw reload
</code></pre>
<h2 id="heading-step-2-create-the-docker-compose-file">Step 2: Create the Docker Compose File</h2>
<p>Using a <code>docker-compose.yml</code> file is the professional way to manage Docker containers. It keeps your setup organised in one single place.</p>
<p>To start, create a new folder for your project and enter it:</p>
<pre><code class="language-shell">mkdir uptime-kuma &amp;&amp; cd uptime-kuma
</code></pre>
<p>Then create the configuration file:</p>
<pre><code class="language-shell">nano docker-compose.yml
</code></pre>
<p>Paste the following code into the editor:</p>
<pre><code class="language-yaml">services:
  uptime-kuma:
    image: louislam/uptime-kuma:2
    restart: unless-stopped
    volumes:
      - ./data:/app/data
    ports:
      - "3001:3001"
</code></pre>
<p><strong>Note</strong>: The <code>./data:/app/data</code> line is very important. It saves your database in a normal folder on your machine, making it easy to back up later.</p>
<p>Finally, save and exit: Press <code>CTRL + X</code>, then <code>Y</code>, then <code>Enter</code>.</p>
<h2 id="heading-step-3-start-the-application">Step 3: Start the Application</h2>
<p>Now, tell Docker to read your file and start the monitoring service in the background.</p>
<pre><code class="language-shell">docker compose up -d
</code></pre>
<p><strong>How to verify:</strong> Docker will download the files. When it finishes, your terminal should print <code>Started uptime-kuma</code>.</p>
<h2 id="heading-step-4-access-the-dashboard">Step 4: Access the Dashboard</h2>
<p>To access the dashboard, first open your web browser and go to <code>http://localhost:3001</code> (or your machine's local IP address).</p>
<p>When asked to choose the database, select <strong>SQLite</strong>. It's simple, fast, and requires no extra setup.</p>
<p>Then create an account and choose a secure admin username and password.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/02913589-020e-4a8a-aa7a-1bf70a9244c6.png" alt="02913589-020e-4a8a-aa7a-1bf70a9244c6" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-step-5-use-case-monitor-a-website-and-send-discord-alerts">Step 5: Use Case – Monitor a Website and Send Discord Alerts</h2>
<p>Now you'll put Uptime Kuma to work by monitoring a live website and setting up an alert. Just follow these steps:</p>
<ol>
<li><p>Click Add New Monitor.</p>
</li>
<li><p>Set the Monitor Type to <code>HTTP(s)</code>.</p>
</li>
<li><p>Give it a Friendly Name (e.g., "My Blog") and enter your website's URL.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/74567f1e-acc4-480f-b969-7883e01aa459.png" alt="74567f1e-acc4-480f-b969-7883e01aa459" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-pro-tip-how-to-fix-down-errors-bot-protection">Pro-Tip: How to Fix "Down" Errors (Bot Protection)</h3>
<p>If your site uses strict security, it might block Uptime Kuma and say your site is "Down" with a 403 Forbidden error.</p>
<p><strong>The Fix:</strong> Scroll down to Advanced, find the User Agent box, and paste this text to make Uptime Kuma look like a normal Chrome browser:</p>
<p><code>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36</code></p>
<h3 id="heading-add-a-discord-alert">Add a Discord Alert</h3>
<p>To get a message on your phone when your site goes down:</p>
<ol>
<li><p>On the right side of the monitor screen, click Setup Notification.</p>
</li>
<li><p>Select Discord from the dropdown list.</p>
</li>
<li><p>Paste a Discord Webhook URL (you can create one in your Discord server settings under Integrations).</p>
</li>
<li><p>Click Test to receive a test ping, then click Save.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congratulations! You just took control of your server health. By deploying Uptime Kuma, you replaced an expensive SaaS subscription with a powerful, free monitoring tool that alerts you the second a project goes offline.</p>
<p><strong>Let’s connect!</strong> I am a developer and technical writer specialising in writing step-by-step guides and workflows. You can find my latest projects on my <a href="https://blog.abdultalha.tech/portfolio"><strong>Technical Writing Portfolio</strong></a> or reach out to me directly on <a href="https://www.linkedin.com/in/abdul-talha/"><strong>LinkedIn</strong></a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Self-Host AFFiNE on Windows with WSL and Docker ]]>
                </title>
                <description>
                    <![CDATA[ Depending on cloud apps means that you don't truly own your notes. If your internet goes down or if the company changes its rules, you could lose access. In this article, you'll learn how to build you ]]>
                </description>
                <link>https://www.freecodecamp.org/news/self-host-affine-windows/</link>
                <guid isPermaLink="false">69b2e3051be92d8f177bf807</guid>
                
                    <category>
                        <![CDATA[ self-hosted ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Open Source ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ WSL ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Abdul Talha ]]>
                </dc:creator>
                <pubDate>Thu, 12 Mar 2026 16:00:05 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/950eee10-aa2c-4071-9c40-abaf759f6d10.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Depending on cloud apps means that you don't truly own your notes. If your internet goes down or if the company changes its rules, you could lose access.</p>
<p>In this article, you'll learn how to build your own private workspace using AFFiNE. You'll use Docker Compose to link three separate pieces of software together:</p>
<ul>
<li><p>The AFFiNE Core application.</p>
</li>
<li><p>A PostgreSQL database to store your notes and pages.</p>
</li>
<li><p>A Redis cache to make the app run fast and smooth.</p>
</li>
</ul>
<p>By the end of this article, you'll have a fully functional web app running on your own computer that works just like the cloud version of Notion.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-is-affine">What is AFFiNE?</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-step-1-preparing-your-workspace">Step 1: Preparing Your Workspace</a></p>
</li>
<li><p><a href="#heading-step-2-getting-the-official-setup-files">Step 2: Getting the Official Setup Files</a></p>
</li>
<li><p><a href="#heading-step-3-configuring-your-environment-env">Step 3: Configuring Your Environment (.env)</a></p>
</li>
<li><p><a href="#heading-step-4-launching-the-system">Step 4: Launching the System</a></p>
</li>
<li><p><a href="#heading-step-5-accessing-the-admin-panel">Step 5: Accessing the Admin Panel</a></p>
</li>
<li><p><a href="#heading-step-6-configuration-making-it-yours">Step 6: Configuration (Making It Yours)</a></p>
</li>
<li><p><a href="#heading-step-7-connecting-the-desktop-app-optional">Step 7: Connecting the Desktop App (Optional)</a></p>
</li>
<li><p><a href="#heading-step-8-stopping-the-server-and-safe-backups">Step 8: Stopping the Server and Safe Backups</a></p>
</li>
<li><p><a href="#heading-step-9-how-to-upgrade-later">Step 9: How to Upgrade Later</a></p>
</li>
<li><p><a href="#heading-common-installation-errors-and-troubleshooting">Common Installation Errors and Troubleshooting</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-affine">What is AFFiNE?</h2>
<p>AFFiNE is an "all-in-one" workspace that combines the powers of writing, drawing, and planning.</p>
<p>While tools like Notion focus on documents and Miro focus on whiteboards, AFFiNE lets you do both in a single space. You can turn your written notes into a visual canvas with one click. This makes it perfect for brainstorming, tracking tasks, and managing your personal knowledge.</p>
<h3 id="heading-the-power-of-self-hosting">The Power of Self-Hosting</h3>
<p>While AFFiNE offers a cloud version, hosting it yourself gives you three major benefits:</p>
<ul>
<li><p><strong>Total data ownership:</strong> Your notes never leave your machine. You own the database.</p>
</li>
<li><p><strong>Privacy in the AI age:</strong> No big tech company can scan your private ideas or use them for AI training.</p>
</li>
<li><p><strong>Real DevOps skills:</strong> Learning how to manage Docker inside WSL is a high-value skill for any modern developer.</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow this article, make sure you have these tools ready on your machine:</p>
<ul>
<li><p><strong>WSL 2 Installation:</strong> You must have WSL installed if you are using Windows (I am using Ubuntu for this guide).</p>
</li>
<li><p><strong>Docker and Docker Compose:</strong> These must be installed and running on your machine.</p>
</li>
<li><p><strong>Linux Terminal Commands:</strong> You should be familiar with basic commands like <code>mkdir</code>, <code>cd</code>, and <code>wget</code>.</p>
</li>
</ul>
<h2 id="heading-step-1-preparing-your-workspace">Step 1: Preparing Your Workspace</h2>
<p>To start, create a folder for your AFFiNE files. This keeps your data in one organised place.</p>
<p>Then open your WSL terminal and run these commands:</p>
<pre><code class="language-shell">mkdir affine
cd affine
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/021e4aef-ede1-4bec-b96e-2acaea9d8f40.png" alt="A terminal Showing the commands mkdir and cd" style="display:block;margin:0 auto" width="1919" height="392" loading="lazy">

<h2 id="heading-step-2-getting-the-official-setup-files">Step 2: Getting the Official Setup Files</h2>
<p>You will download the official configuration files directly from the AFFiNE. In your WSL terminal, run these two commands:</p>
<ol>
<li>Download the Docker Compose file:</li>
</ol>
<pre><code class="language-shell">wget -O docker-compose.yml https://github.com/toeverything/affine/releases/latest/download/docker-compose.yml
</code></pre>
<ol>
<li>Download the Environment template:</li>
</ol>
<pre><code class="language-shell">wget -O .env https://github.com/toeverything/affine/releases/latest/download/default.env.example
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/5b366a5f-b426-4e70-95c0-b469f40d6af5.png" alt="A terminal Showing the commands to download affine" style="display:block;margin:0 auto" width="1905" height="808" loading="lazy">

<h2 id="heading-step-3-configuring-your-environment-env">Step 3: Configuring Your Environment (.env)</h2>
<p>The <code>.env</code> file is like a hidden settings sheet. It keeps your passwords and setup details private.</p>
<p>To edit this file, you can use Nano, which is a simple text editor built into your Linux terminal. Follow these steps to update your settings:</p>
<ol>
<li><p><strong>Open the file with Nano:</strong></p>
<pre><code class="language-shell">nano .env
</code></pre>
</li>
<li><p><strong>Update the settings:</strong> Use your arrow keys to move around the file. Update these specific lines to match the locations below. This keeps your data safely inside your new <code>affine</code> folder:</p>
<pre><code class="language-plaintext">DB_DATA_LOCATION=./postgres
UPLOAD_LOCATION=./storage
CONFIG_LOCATION=./config

DB_USERNAME=affine
DB_PASSWORD=
DB_DATABASE=affine
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/d0f4a358-e221-45d3-94df-d97b606b4afc.png" alt="A terminal to change the values in env file" style="display:block;margin:0 auto" width="1909" height="795" loading="lazy">

<p><strong>Save and Exit:</strong> Press Ctrl + O to save.</p>
<ul>
<li><p>Press <strong>Enter</strong> to confirm the filename.</p>
</li>
<li><p>Press <strong>Ctrl + X</strong> to exit the editor.</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-step-4-launching-the-system">Step 4: Launching the System</h2>
<p>Run this Docker command to build your workspace:</p>
<pre><code class="language-shell">docker compose up -d
</code></pre>
<p>Docker will download the AFFiNE app and a Postgres database. The <code>-d</code> flag means it will run quietly in the background.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/407237bd-f805-4fca-b15c-6bf001f467e7.png" alt="A terminal Showing the commands for docker compose" style="display:block;margin:0 auto" width="1895" height="224" loading="lazy">

<h2 id="heading-step-5-accessing-the-admin-panel">Step 5: Accessing the Admin Panel</h2>
<p>Once the terminal says "Started," your private server is live!</p>
<p>Open your web browser and go to:</p>
<pre><code class="language-plaintext">http://localhost:3010/
</code></pre>
<p>The first time you visit this page, you must create an admin account. This is the master key to your server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/780fafda-0afd-4b67-a2fa-6248b4d5d4f3.png" alt="creating an Admin account" style="display:block;margin:0 auto" width="982" height="1054" loading="lazy">

<h2 id="heading-step-6-configuration-making-it-yours">Step 6: Configuration (Making It Yours)</h2>
<p>There are two ways to configure your server.</p>
<h3 id="heading-the-easy-way-admin-panel"><strong>The Easy Way: Admin Panel</strong></h3>
<p>In your browser, go to <code>http://localhost:3010/admin/settings</code>. You can change your server name or set up emails here.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/0f8d4e97-7a47-4328-8e91-a36582d47143.png" alt="Overview of the settings page" style="display:block;margin:0 auto" width="1919" height="870" loading="lazy">

<h3 id="heading-the-developer-way-config-file"><strong>The Developer Way: Config File</strong></h3>
<p>You can also create a <code>config.json</code> file inside your <code>./config</code> folder.</p>
<pre><code class="language-json">{
  "$schema": "https://github.com/toeverything/affine/releases/latest/download/config.schema.json",
  "server": {
    "name": "My Private Workspace"
  }
}
</code></pre>
<h2 id="heading-step-7-connecting-the-desktop-app-optional">Step 7: Connecting the Desktop App (Optional)</h2>
<p>You don't have to use the browser. You can connect the official AFFiNE desktop app.</p>
<ol>
<li><p>Download the AFFiNE desktop app.</p>
</li>
<li><p>Click the workspace list panel in the top left corner.</p>
</li>
<li><p>Click "Add Server" and enter <code>http://localhost:3010</code>.</p>
</li>
<li><p>Log in with your account.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/2c668ed4-3552-420f-9217-e5f8d09f311c.png" alt="Connecting your local server to Affine Server" style="display:block;margin:0 auto" width="498" height="610" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6729b04417afd6915f5c2e3e/3a12b7f6-33b9-497e-8684-7fd7a09d8c42.png" alt="Overview of Workspace" style="display:block;margin:0 auto" width="1919" height="869" loading="lazy">

<h2 id="heading-step-8-stopping-the-server-and-safe-backups">Step 8: Stopping the Server and Safe Backups</h2>
<p>You must turn your server off safely before you back up your notes.</p>
<p>To do that, run this command:</p>
<pre><code class="language-shell">docker compose down
</code></pre>
<p>Once it stops, you can safely copy your entire <code>affine</code> folder to a safe place.</p>
<h2 id="heading-step-9-how-to-upgrade-later">Step 9: How to Upgrade Later</h2>
<p>When AFFiNE releases a new version, run these commands inside your <code>affine</code> folder:</p>
<ol>
<li>Download the newest blueprint:</li>
</ol>
<pre><code class="language-shell">wget -O docker-compose.yml https://github.com/toeverything/affine/releases/latest/download/docker-compose.yml
</code></pre>
<ol>
<li>Pull the new images and restart:</li>
</ol>
<pre><code class="language-shell">docker compose pull
docker compose up -d
</code></pre>
<h2 id="heading-common-installation-errors-and-troubleshooting">Common Installation Errors and Troubleshooting</h2>
<h3 id="heading-1-docker-is-not-running">1. Docker is Not Running</h3>
<ul>
<li><p><strong>The Error:</strong> Terminal says <code>docker: command not found</code>.</p>
</li>
<li><p><strong>The Fix:</strong> Open the Docker Desktop app on Windows and wait for it to start.</p>
</li>
</ul>
<h3 id="heading-2-docker-is-not-connected-to-wsl">2. Docker is Not Connected to WSL</h3>
<ul>
<li><strong>The Fix:</strong> In Docker Desktop, go to <strong>Settings &gt; Resources &gt; WSL Integration</strong> and turn it ON for your distro.</li>
</ul>
<h3 id="heading-3-the-port-is-already-in-use">3. The Port is Already in Use</h3>
<ul>
<li><strong>The Fix:</strong> Open <code>docker-compose.yml</code>. Change <code>"3010:3010"</code> to <code>"4000:3010"</code>. You will now visit <code>localhost:4000</code>.</li>
</ul>
<h3 id="heading-4-permission-denied">4. Permission Denied</h3>
<ul>
<li><strong>The Fix:</strong> If you cannot delete a folder, use the sudo command: <code>sudo rm -rf affine/</code>.</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you've successfully built a self-hosted, private workspace. You practised using WSL, Docker Compose, and Postgres. These are valuable skills for any developer.</p>
<p><strong>Your next steps:</strong></p>
<ol>
<li><p>Create a note in AFFiNE documenting what you learned.</p>
</li>
<li><p>Turn off your server (<code>docker compose down</code>) and copy your folder to a backup drive.</p>
</li>
<li><p>Explore Cloudflare Tunnels if you want to access your server from your phone!</p>
</li>
</ol>
<p>Self-hosting takes a little work, but the privacy is worth it.</p>
<p><strong>Let’s connect!</strong> You can find my latest work on my <a href="https://blog.abdultalha.tech/portfolio"><strong>Technical Writing Portfolio</strong></a> or reach out to me on <a href="https://www.linkedin.com/in/abdul-talha/"><strong>LinkedIn</strong></a>.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
