
ai
Part 3: How to Actually Use n8n
• <p><em>Series: Self-Hosted Automation with n8n — part 3 of 3</em></p><p>You know what n8n is (part 1) and you have your own instance running (part 2). Now the fun part: building things. We’ll walk from your very first workflow to handling data between nodes, dropping into code when you need it, and wiring up an AI agent - all on the instance you already control.</p><h2 id="the-building-blocks">The building blocks</h2> <p>Open your instance at <code>http://localhost:5678</code> and create a new workflow. You’re looking at a blank <strong>canvas</strong>. Everything you build here is made of three things:</p><ul> <li><strong>Trigger</strong> - the first node, the event that starts the run. A schedule, a webhook, a manual click while testing.</li> <li><strong>Nodes</strong> - each step after the trigger. An HTTP request, a database query, a transformation, an <code>if</code>.</li> <li><strong>Connections</strong> - the wires between them, carrying <strong>items</strong> (rows of JSON) from one node to the next.</li> </ul> <p>That’s the whole game. Data enters at the trigger and flows down the wires, and each node reshapes it a little.</p><h2 id="your-first-workflow">Your first workflow</h2> <p>Let’s build the “hello world” of n8n: fetch some data on a schedule and send it somewhere. We’ll keep it concrete.</p><ol> <li><strong>Add a trigger.</strong> Click the <code>+</code>, search for <strong>Schedule Trigger</strong>, and set it to run once a day. (While building, ignore the schedule and just use the <strong>Execute workflow</strong> button to run on demand.)</li> <li><strong>Add an HTTP Request node.</strong> Wire it after the trigger. Point it at any public API - say a weather or exchange-rate endpoint - with method <code>GET</code>. Run the workflow and you’ll see the raw JSON response appear in the node’s output panel.</li> <li><strong>Inspect the data.</strong> Click the node’s output. n8n shows you the exact items that came back - table view, JSON view, and schema view. This panel is where you’ll spend most of your building time.</li> <li><strong>Do something with it.</strong> Add a node for wherever the result should go - a Slack message, a row in Google Sheets, an email, a database insert. Map the fields from the previous step into it.</li> </ol> <p>Run it once with <strong>Execute workflow</strong>, confirm each node lit up green, then flip the toggle to <strong>Active</strong> in the top right. It now runs on its own.</p><h2 id="working-with-data-items-and-expressions">Working with data: items and expressions</h2> <p>The thing that trips people up first is how data moves. Every node outputs a <strong>list of items</strong>, and the next node runs <strong>once per item</strong>. If the HTTP node returns 10 records, the node after it fires 10 times - one per record. This is what makes n8n scale nicely: you rarely write loops, the item model loops for you.</p><p>To pull a value from a previous step into a field, you use an <strong>expression</strong>, written in <code>{{ }}</code>:</p><pre><code>{{ $json.temperature }} </code></pre> <p><code>$json</code> is the current item’s data. You can reference earlier nodes by name too:</p><pre><code>{{ $('HTTP Request').item.json.city }} </code></pre> <p>Expressions are just JavaScript, so you can transform inline:</p><pre><code>{{ $json.price * 1.21 }} {{ $json.name.toUpperCase() }} </code></pre> <p>For quick reshaping without code, the <strong>Edit Fields (Set)</strong> node lets you rename, add, and drop fields with a form. <strong>Filter</strong> drops items that don’t match a condition; <strong>IF</strong> splits the flow into two branches; <strong>Merge</strong> brings branches back together.</p><h2 id="when-you-need-the-code-node">When you need the Code node</h2> <p>Sometimes a visual node can’t express what you want. That’s what the <strong>Code</strong> node is for - write JavaScript (or Python) directly and return items. A simple example that reshapes the incoming data:</p><pre><code class="language-javascript">// Runs once, receives all incoming items in $input const items = $input.all(); return items.map(item => ({ json: { fullName: `${item.json.firstName} ${item.json.lastName}`, isAdult: item.json.age >= 18, }, })); </code></pre> <p>The rule to remember: a Code node must <strong>return an array of objects, each with a <code>json</code> key</strong>. That’s the shape every node speaks.</p><p>A modern-n8n note: <strong>Code nodes now run in isolated task runners by default</strong>, so an accidental infinite loop won’t take down your whole instance - and for security, access to system environment variables from inside a node is blocked by default.</p><h2 id="adding-an-ai-step">Adding an AI step</h2> <p>This is where n8n has gotten genuinely powerful. Add an <strong>AI Agent</strong> node, connect a <strong>chat model</strong> (OpenAI, Anthropic, or a locally hosted model if you want it fully private), and give the agent a goal in plain language.</p><p>The trick that makes it more than a chatbot: you can attach your <strong>other nodes as tools</strong>. Give the agent an HTTP Request tool and a database tool, and it decides <em>when</em> to call them. So a workflow like “read this support email, look up the customer, and draft a reply” becomes: trigger on new email → AI Agent (with a lookup tool) → draft response. The model handles the judgment, n8n handles the plumbing.</p><p>Start small here - one tool, a tight prompt - and expand once you trust what it’s doing.</p><h2 id="debugging-and-error-handling">Debugging and error handling</h2> <ul> <li><strong>Run node by node.</strong> Each node keeps its last output. Click through them after a run to see exactly where data changed shape or a request failed.</li> <li><strong>Pin data while building.</strong> Pin a node’s output so downstream nodes get consistent test data and you’re not hammering a real API on every run.</li> <li><strong>Add an error path.</strong> Attach an <strong>Error Trigger</strong> workflow, or set a node’s <em>continue on fail</em> option, so one bad item doesn’t silently kill the run. For anything important, route failures to a Slack or email alert.</li> <li><strong>Check the executions list.</strong> Every run is logged with its full data, so when something breaks at 3am you can open the exact failed execution and see why.</li> </ul> <h2 id="a-few-closing-tips">A few closing tips</h2> <ul> <li><strong>Steal from templates.</strong> n8n has a large library of ready-made workflows. Importing one and taking it apart is the fastest way to learn how the nodes fit together.</li> <li><strong>Name your nodes.</strong> “HTTP Request1” tells you nothing in three weeks. Rename nodes to what they do - expressions that reference them stay readable too.</li> <li><strong>Keep secrets in credentials, not in nodes.</strong> Use n8n’s credential store for API keys so they’re encrypted and reusable, never pasted into a field.</li> <li><strong>Build incrementally.</strong> Add one node, run, check the output, add the next. Debugging a 20-node workflow you wrote all at once is nobody’s idea of fun.</li> </ul> <h2 id="end-of-the-series">End of the series</h2> <p>That’s the full arc: <strong>what</strong> n8n is (part 1), <strong>standing up</strong> your own instance with Docker (part 2), and <strong>building</strong> with it (part 3). You now have a private automation platform on infrastructure you control - one that connects your apps, runs your logic, and can put an AI agent in the loop, all without a per-execution bill or your data leaving your server.</p><p>Pick one boring task you do every week, and go automate it.</p><hr> <p><em>Written August 2026, against n8n 2.x (Community Edition). Node names and options evolve between releases - the output panel and docs.n8n.io are your source of truth.</em></p>

ai
Part 2: Install Your Own n8n with Docker
• <p><em>Series: Self-Hosted Automation with n8n — part 2 of 3</em></p><p>In part 1 we covered what n8n is and why you’d want to run it yourself. Now we make that real. By the end of this article you’ll have a private n8n instance running on your own machine or server, with your workflows and credentials stored on infrastructure you control.</p><p>We’ll go from the fastest possible one-liner to a proper production-minded setup with PostgreSQL - pick whichever matches how serious you’re getting.</p><h2 id="why-docker-and-why-not-npm">Why Docker (and why not npm)</h2> <p>Docker bundles n8n and everything it needs into one predictable container. No wrestling with Node versions, no “works on my machine”. You pull an image, run it, and it behaves the same on your laptop, a €4 VPS, or a Raspberry Pi in the closet.</p><p>It’s also the direction n8n itself is heading: the old <code>npm install n8n</code> / <code>npx n8n</code> path is being retired, and <strong>from n8n 3.0 (launching October 2026) n8n is distributed through Docker only</strong>. So Docker isn’t just convenient anymore - it’s the path.</p><h2 id="prerequisites">Prerequisites</h2> <ul> <li><strong>Docker</strong> and <strong>Docker Compose</strong> installed. On a fresh Linux server, the official convenience script gets you both:</li> </ul> <pre><code class="language-bash">curl -fsSL https://get.docker.com | sh </code></pre> <ul> <li>On macOS or Windows, install <strong>Docker Desktop</strong>, which includes Compose.</li> </ul> <p>Check they’re there:</p><pre><code class="language-bash">docker --version docker compose version </code></pre> <h2 id="the-fast-way-one-command">The fast way: one command</h2> <p>n8n ships an official installer that writes a sensible Docker Compose setup for you and starts everything:</p><pre><code class="language-bash">curl -fsSL https://get.n8n.io | sh </code></pre> <p>It checks that Docker is running, creates an <code>./n8n</code> folder with a <code>compose.yml</code> and a <code>.env</code> (with unique secrets generated for you), pulls the images, and starts the stack. When it’s done you’ll see something like:</p><pre><code>n8n is running at: http://localhost:5678 Data stored in: ./n8n (Docker volume: n8n-data) </code></pre> <p>Open <strong><a href="http://localhost:5678">http://localhost:5678</a></strong>, create your owner account, and you’re in. To stop it later: <code>docker compose -f ./n8n/compose.yml down</code>. This is the right choice for a fresh install when you don’t want to think about the details.</p><h2 id="the-manual-way-understand-whats-happening">The manual way: understand what’s happening</h2> <p>If you’d rather know exactly what you’re running, do it by hand. First create a <strong>named volume</strong> so your data survives container restarts, then run the image:</p><pre><code class="language-bash">docker volume create n8n_data docker run -d --restart unless-stopped \ --name n8n \ -p 5678:5678 \ -e GENERIC_TIMEZONE="Europe/Prague" \ -e TZ="Europe/Prague" \ -v n8n_data:/home/node/.n8n \ docker.n8n.io/n8nio/n8n </code></pre> <p>What each part does:</p><ul> <li><strong><code>docker.n8n.io/n8nio/n8n</code></strong> is the official image. Use this, not random mirrors.</li> <li><strong><code>-p 5678:5678</code></strong> maps n8n’s port to your host, so you reach it at <code>http://localhost:5678</code>.</li> <li><strong><code>-v n8n_data:/home/node/.n8n</code></strong> is the important one - n8n keeps its SQLite database, encryption key, and settings in <code>/home/node/.n8n</code>. Mount a volume there or you <em>will</em> lose everything on the next restart.</li> <li><strong><code>-d --restart unless-stopped</code></strong> runs it in the background and brings it back up after a reboot. (The docs often show <code>-it --rm</code> instead - that’s for a throwaway test instance that deletes itself when stopped.)</li> <li><strong><code>GENERIC_TIMEZONE</code> / <code>TZ</code></strong> make schedule triggers fire at the times you actually mean.</li> </ul> <p>One version note: on <strong>n8n 1.x</strong> you also needed <code>-e N8N_RUNNERS_ENABLED=true</code> to enable task runners. From <strong>n8n 2.0 that’s the default</strong> and you no longer set it.</p><h2 id="a-production-minded-setup-docker-compose--postgresql">A production-minded setup: Docker Compose + PostgreSQL</h2> <p>The default SQLite database is fine for trying things out, but for anything you’ll rely on, put <strong>PostgreSQL</strong> behind n8n. Compose lets you define both services in one file. Create <code>compose.yaml</code>:</p><pre><code class="language-yaml">services: postgres: image: postgres:16 restart: unless-stopped environment: - POSTGRES_USER=n8n - POSTGRES_PASSWORD=change-me-please - POSTGRES_DB=n8n volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"] interval: 10s timeout: 5s retries: 5 n8n: image: docker.n8n.io/n8nio/n8n:latest restart: unless-stopped ports: - "5678:5678" environment: - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PORT=5432 - DB_POSTGRESDB_DATABASE=n8n - DB_POSTGRESDB_USER=n8n - DB_POSTGRESDB_PASSWORD=change-me-please - N8N_HOST=n8n.example.com - N8N_PROTOCOL=https - WEBHOOK_URL=https://n8n.example.com/ - GENERIC_TIMEZONE=Europe/Prague - TZ=Europe/Prague - N8N_ENCRYPTION_KEY=replace-with-a-long-random-string volumes: - n8n_data:/home/node/.n8n depends_on: postgres: condition: service_healthy volumes: postgres_data: n8n_data: </code></pre> <p>Start it, and watch the logs on the first run:</p><pre><code class="language-bash">docker compose up -d docker compose logs -f n8n </code></pre> <p>Two things worth calling out:</p><ul> <li><strong><code>N8N_ENCRYPTION_KEY</code></strong> encrypts your saved credentials. n8n generates one automatically inside the volume, but setting it explicitly means it stays stable if you ever move or rebuild the instance - <strong>back this value up somewhere safe</strong>, because without it your stored credentials become unreadable.</li> <li><strong><code>WEBHOOK_URL</code></strong> must match the public URL where n8n actually lives, or incoming webhooks from external services won’t line up.</li> </ul> <p>For real HTTPS in front of this, put a reverse proxy like <strong>Caddy</strong>, <strong>Traefik</strong>, or <strong>Nginx Proxy Manager</strong> ahead of n8n - Caddy and Traefik will fetch and renew TLS certificates for you automatically.</p><h2 id="webhooks-on-localhost-the-one-gotcha">Webhooks on localhost: the one gotcha</h2> <p>If you’re running locally, external services like Stripe, GitHub, or Slack <strong>cannot reach <code>localhost</code></strong> to deliver webhooks - it isn’t on the public internet. For quick testing, start the container with n8n’s built-in tunnel:</p><pre><code class="language-bash">docker run -it --rm --name n8n \ -p 5678:5678 \ -v n8n_data:/home/node/.n8n \ docker.n8n.io/n8nio/n8n start --tunnel </code></pre> <p>That’s for development only. For anything lasting, expose n8n through a real domain (the Compose setup above) or a tunneling service like <strong>Cloudflare Tunnel</strong> or <strong>ngrok</strong>.</p><h2 id="updating">Updating</h2> <p>Because your data lives in the volume - not in the container - updating is just pulling a newer image and recreating the container:</p><pre><code class="language-bash">docker compose pull docker compose up -d </code></pre> <p>Your workflows and credentials are untouched. <strong>Before a major version jump</strong> (say 1.x to 2.x), take a backup first and skim the release notes for breaking changes.</p><h2 id="a-few-closing-tips">A few closing tips</h2> <ul> <li><strong>Always mount the volume.</strong> The single most common “I lost my workflows” story is a <code>docker run</code> without <code>-v</code>.</li> <li><strong>Save your encryption key.</strong> Treat <code>N8N_ENCRYPTION_KEY</code> like a password - losing it means losing access to every stored credential.</li> <li><strong>Use Postgres for anything real.</strong> SQLite is great for a first look, but a proper database saves you pain once executions pile up.</li> <li><strong>Don’t expose port 5678 raw to the internet.</strong> Put a reverse proxy and HTTPS in front of it, and turn on n8n’s user management.</li> </ul> <h2 id="next-up">Next up</h2> <p>You’ve got a running instance. In [part 3] we’ll actually use it - building your first workflow, working with the data flowing between nodes, dropping into code when you need to, and wiring up an AI agent.</p><hr> <p><em>Written August 2026, tested against n8n 2.x with Docker Compose v2. Commands and defaults shift between versions - check docs.n8n.io before a production deploy.</em></p>

ai
Part 1: What Is n8n, and Why Everyone Keeps Bringing It Up
• <p><em>Series: Self-Hosted Automation with n8n — part 1 of 3</em></p><p>You have probably hit that moment: a handful of tools that almost talk to each other, a spreadsheet that someone updates by hand every morning, and three browser tabs open just to move one piece of data from A to B. That gap between your apps is exactly what <strong>n8n</strong> fills - and unlike most of its competitors, you can run the whole thing on your own machine, for free.</p><p>This is part one of a short series. Here we cover <em>what</em> n8n is and how it thinks. In part 2 we install our own instance with Docker, and in part 3 we actually build workflows.</p><h2 id="what-n8n-actually-is">What n8n actually is</h2> <p>n8n (you say it “n-eight-n”, short for <em>nodes-to-nodes</em>) is an <strong>open-source workflow automation tool</strong>. You build automations visually by dragging boxes onto a canvas and wiring them together. Each box is a <strong>node</strong> - an API call, a data transformation, a trigger, or a piece of flow control like an <code>if</code> or a loop. Data flows left to right through the wires, and you can watch exactly what each step received and produced.</p><p>It sits in the same category as Zapier and Make, but with two differences that matter: it’s <strong>fair-code and self-hostable</strong>, and it doesn’t stop you the moment your logic gets interesting. When a no-code path isn’t enough, you drop into a JavaScript or Python node and keep going.</p><h2 id="how-it-thinks-triggers-nodes-connections">How it thinks: triggers, nodes, connections</h2> <p>Every workflow starts with a <strong>trigger</strong> - the event that kicks things off. That might be a schedule (“every day at 8am”), an incoming <strong>webhook</strong> (some external service calls your workflow), a new email, a new row in a database, or a manual “run now” while you’re building.</p><p>After the trigger, each node receives a list of <strong>items</strong> - think of them as rows of JSON - does something with them, and passes the result to the next node. Because everything is JSON moving down a pipe, you can inspect it at any point, reshape it, filter it, split it, or merge two branches back together. This is the whole mental model, and once it clicks the rest is just learning which nodes exist.</p><h2 id="where-n8n-fits">Where n8n fits</h2> <p>The usual comparison is Zapier and Make, and the honest answer is that n8n wins when workflows get <strong>complex or high-volume</strong>, and loses when you want the absolute simplest two-step zap with zero setup.</p><ul> <li><strong>You outgrew Zapier’s pricing.</strong> Self-hosted n8n has no per-execution or per-workflow fee - you run as much as your server can handle.</li> <li><strong>You need real logic.</strong> Branching, loops, merging, error handling, custom code - n8n treats these as first-class, not as paid add-ons.</li> <li><strong>You care where your data lives.</strong> Self-hosting keeps execution logs and credentials on infrastructure you control, which matters for anything touching personal or regulated data.</li> </ul> <p>The catch: it expects you to be comfortable with a little bit of technical setup. Basic command-line and API familiarity goes a long way, even if you never write a line of code in a workflow.</p><h2 id="fair-code-not-quite-open-source">Fair-code, not quite open source</h2> <p>n8n is licensed under the <strong>Sustainable Use License</strong> - often called <em>fair-code</em>. In plain terms: the <strong>Community Edition is free</strong>, you can self-host it, use it internally, and build on it. There are paid <strong>Business</strong> and <strong>Enterprise</strong> editions that add things like SSO, role-based access control, multiple environments, and support - aimed at larger teams rather than individuals.</p><p>For a solo developer or a small team running your own instance, the free Community Edition is genuinely all you need, and that’s what the rest of this series uses.</p><h2 id="the-ai-angle">The AI angle</h2> <p>The reason n8n’s name comes up so often lately is AI. It ships <strong>native AI nodes</strong> for the major LLM providers - OpenAI, Anthropic, and others - plus an <strong>AI Agent</strong> node that can call your other workflow steps as <em>tools</em>. That means you can build something like “read this email, decide what it’s about, look up the customer in our database, and draft a reply” as a single visual workflow, with the model making the decisions and n8n doing the plumbing.</p><p>If you want that fully private, you can point those AI nodes at a locally hosted model instead of a cloud API - which pairs nicely with self-hosting the rest.</p><h2 id="a-few-things-to-know-before-you-start">A few things to know before you start</h2> <ul> <li><strong>n8n is the glue, not the destination.</strong> Its value is connecting the tools you already use. Start by listing a boring, repetitive task you do every week - that’s your first workflow.</li> <li><strong>Self-hosting is a privacy advantage, but only up to a point.</strong> If your workflow forwards data to a cloud API, that data still leaves your server. Check what each connected service does with it.</li> <li><strong>You don’t need to code.</strong> But knowing what an API and a webhook are will make the whole thing click much faster.</li> </ul> <h2 id="next-up">Next up</h2> <p>That’s the <em>what</em> and <em>why</em>. In [part 2] we’ll get our hands dirty and stand up a private n8n instance with Docker - the fast one-command way, and a more production-minded setup with PostgreSQL behind it.</p><hr> <p><em>Written August 2026, against n8n 2.x (Community Edition). n8n moves quickly - always check the current docs at docs.n8n.io for the latest node and licensing details.</em></p>

ai
Part 3: Fine-Tuning with LoRA on Your Own Data, Right on Your MacBook
• <p><em>Series: AI on Apple Silicon — part 3 of 3</em></p><p>We’ve reached the most fun part. You can run a model (part 1) and shrink it with quantization (part 2). Now we’ll teach it something it doesn’t know out of the box - we’ll fine-tune it on <strong>your own data</strong>. And the whole thing runs on a MacBook in tens of minutes, without a single cent spent on the cloud.</p><h2 id="lora-and-qlora-briefly">LoRA and QLoRA, briefly</h2> <p>Fine-tuning a whole model means rewriting all of its weights - that’s memory-intensive and unrealistic on a laptop. <strong>LoRA</strong> (Low-Rank Adaptation) takes a smarter route: it freezes the original weights and adds small “adapters” alongside them - a handful of new matrices that are the only thing trained. You train just a fraction of the parameters (often less than one percent), yet the result behaves as if you’d fine-tuned the whole model.</p><p><strong>QLoRA</strong> is simply LoRA running on top of a quantized model. When you point <code>mlx_lm.lora</code> at the 4-bit model from the previous part, it automatically runs in QLoRA mode - and the memory footprint drops enough that you can fine-tune an 8B model even on a 16 GB Mac.</p><h2 id="step-1-prepare-your-data">Step 1: Prepare your data</h2> <p>MLX LM reads training data from a folder containing <code>train.jsonl</code> and <code>valid.jsonl</code> files (and optionally <code>test.jsonl</code>). Each line is one example. Among others, the chat format is supported:</p><pre><code class="language-json">{"messages": [{"role": "user", "content": "Question?"}, {"role": "assistant", "content": "Answer."}]} </code></pre> <p>Or the simpler prompt/completion format:</p><pre><code class="language-json">{"prompt": "Translate to SQL: how many users are there?", "completion": "SELECT COUNT(*) FROM users;"} </code></pre> <p>How many examples? <strong>200-500</strong> quality samples are enough to get started. Bet on consistency and clean data over volume - a few hundred careful examples beat thousands of sloppy ones.</p><p>Keep the folder structure like this:</p><pre><code>data/ train.jsonl valid.jsonl </code></pre> <h2 id="step-2-run-the-training">Step 2: Run the training</h2> <p>The main command is <code>mlx_lm.lora</code>. Point it at a model, the data folder, and a number of iterations:</p><pre><code class="language-bash">mlx_lm.lora \ --model mlx-community/Llama-3.1-8B-Instruct-4bit \ --train \ --data ./data \ --iters 600 </code></pre> <p>Because the model is 4-bit, this runs as QLoRA automatically. During training, <code>mlx-lm</code> continuously prints <strong>train loss</strong> and, every few steps, <strong>validation loss</strong> - those are the numbers you want to see going down.</p><p>If you want more control (learning rate, number of adapted layers, batch size), pass a config YAML with <code>-c</code>:</p><pre><code class="language-bash">mlx_lm.lora --model <model> --train --data ./data -c lora_config.yaml </code></pre> <p>Grab the YAML template from the <code>ml-explore/mlx-lm</code> repository; command-line flags take precedence over values in the file.</p><h2 id="step-3-test-the-adapter">Step 3: Test the adapter</h2> <p>Training saves the <strong>adapters</strong> into a folder (<code>./adapters</code> by default). You try them out by attaching them to the base model at generation time:</p><pre><code class="language-bash">mlx_lm.generate \ --model mlx-community/Llama-3.1-8B-Instruct-4bit \ --adapter-path ./adapters \ --prompt "Your test prompt from the domain you trained on" </code></pre> <p>Compare the output with what the model said <strong>before</strong> fine-tuning (same prompt, without <code>--adapter-path</code>). That’s how you tell whether the fine-tuning took.</p><h2 id="step-4-fuse-the-adapter-into-the-model-for-deployment">Step 4: Fuse the adapter into the model (for deployment)</h2> <p>For convenient deployment you don’t have to carry the adapter separately - <code>mlx_lm.fuse</code> merges it permanently into the weights and produces a standalone model:</p><pre><code class="language-bash">mlx_lm.fuse \ --model mlx-community/Llama-3.1-8B-Instruct-4bit \ --adapter-path ./adapters \ --save-path ./my-fine-tuned-model </code></pre> <p>You then run the result exactly like any other model from part 1 - for instance, serve it straight away with <code>mlx_lm.server</code> and wire it into your editor.</p><h2 id="how-much-time-and-memory-it-takes">How much time and memory it takes</h2> <p>Roughly: <strong>QLoRA on an 8B model</strong> trains on a 16 GB MacBook in the order of tens of minutes (often under an hour). A machine with 32 GB comfortably handles 14B. The nice part is that it’s a one-time investment of your time and electricity - no cloud GPU bills, no data leaving your laptop.</p><h2 id="a-few-closing-tips">A few closing tips</h2> <ul> <li><strong>Fine-tuning isn’t for giving the model knowledge.</strong> For “teaching it facts,” RAG is the better tool. LoRA excels at teaching a model <strong>form</strong> - the style of answers, a specific output format, domain jargon, behavior on a particular task.</li> <li><strong>Start small.</strong> An 8B model and a few hundred examples are ideal for dialing in the whole workflow. Move to bigger models and datasets once you’ve got the process nailed.</li> <li><strong>Watch the validation loss.</strong> When train loss falls but validation rises, the model is overfitting - cut iterations or add data.</li> </ul> <h2 id="end-of-the-series">End of the series</h2> <p>And that’s it. We’ve gone the whole way from <strong>why</strong> (the intro article), through <strong>run it</strong> (part 1) and <strong>shrink it</strong> (part 2), to <strong>adapt it</strong> (part 3). You now have a complete local language-model workshop on your Mac: you can run models, expose them as an API, shrink them with quantization, and fine-tune them on your own data - privately, offline, and for free.</p><hr> <p><em>Tested with <code>mlx-lm</code> 0.30.x on macOS 15+. Always check exact command flags with <code>--help</code>, as they occasionally change between versions.</em></p>

ai
Part 2: Quantization in Practice — One Model in 16, 8, and 4 Bits
• <p><em>Series: AI on Apple Silicon — part 2 of 3</em></p><p>In part 1 we got a model running, and I kept nudging you toward 4-bit variants. In this part we’ll explain why: we’ll take one and the same model, convert it to three different precisions, and see with our own eyes (and our own Activity Monitor) what it does to memory, speed, and output quality.</p><h2 id="what-quantization-is-briefly-and-plainly">What quantization is, briefly and plainly</h2> <p>A model’s weights are a giant table of numbers. In their original form, each number is stored in 16 bits (the bf16 format). <strong>Quantization</strong> means storing those same numbers at lower precision - say 4 bits per weight. That shrinks the model’s footprint in memory to roughly a quarter.</p><p>The catch is that lower precision is coarser. The model “rounds off” its weights, so you lose some quality. The question you actually ask in practice is: <em>how much quality am I willing to trade for the model fitting in memory and running faster?</em> And that’s exactly what we’re about to measure.</p><h2 id="step-1-pick-a-model-and-download-the-full-precision">Step 1: Pick a model and download the full precision</h2> <p>Choose a smaller model so the conversion runs quickly even on a weaker machine. We’ll use <code>mlx_lm.convert</code>, which can download the original from Hugging Face and convert it to MLX format in one go.</p><p>First we’ll prepare the <strong>full 16-bit (bf16) variant</strong> - that’s our reference point for quality:</p><pre><code class="language-bash">mlx_lm.convert \ --hf-path Qwen/Qwen2.5-3B-Instruct \ --mlx-path ./qwen3b-bf16 </code></pre> <h2 id="step-2-convert-to-8-and-4-bits">Step 2: Convert to 8 and 4 bits</h2> <p>The same tool with the <code>-q</code> (quantize) flag produces a quantized version. The default quantization is 4-bit:</p><pre><code class="language-bash"># 4 bits (default) mlx_lm.convert --hf-path Qwen/Qwen2.5-3B-Instruct -q --mlx-path ./qwen3b-4bit # 8 bits mlx_lm.convert --hf-path Qwen/Qwen2.5-3B-Instruct -q --q-bits 8 --mlx-path ./qwen3b-8bit </code></pre> <p>Conversion takes just a few seconds for a small model. The result is three folders with the same model at three precisions.</p><blockquote> <p><strong>Group size:</strong> quantization doesn’t rewrite each weight individually but in groups (64 by default). A smaller group = finer detail, but a larger model. For most people the default is right; tuning it only matters when you’re chasing the last few percent of quality.</p></blockquote> <h2 id="step-3-measure-memory-and-speed">Step 3: Measure memory and speed</h2> <p>Run the same generation through all three variants and watch two numbers: <strong>how much memory</strong> the model uses and <strong>how many tokens per second</strong> it generates. <code>mlx_lm.generate</code> prints the speed for you in verbose mode:</p><pre><code class="language-bash">mlx_lm.generate \ --model ./qwen3b-4bit \ --prompt "List three advantages of unified memory." \ --max-tokens 200 </code></pre> <p>The easiest way to read memory is in Activity Monitor (the Memory column for the Python process), or from the usage MLX prints out. Measure <code>./qwen3b-bf16</code>, <code>./qwen3b-8bit</code>, and <code>./qwen3b-4bit</code> in turn.</p><h2 id="what-youll-roughly-see">What you’ll roughly see</h2> <p>The exact numbers depend on the model and machine, but the ratios always come out similar. For a 7B model, roughly:</p><table> <thead> <tr> <th>Precision</th> <th>Memory (approx.)</th> <th>Relative speed</th> <th>Quality</th> </tr> </thead> <tbody><tr> <td>bf16 (16-bit)</td> <td>~14 GB</td> <td>slowest</td> <td>highest</td> </tr> <tr> <td>8-bit</td> <td>~7 GB</td> <td>faster</td> <td>practically indistinguishable</td> </tr> <tr> <td>4-bit</td> <td>~4.5 GB</td> <td>fastest</td> <td>slightly lower, usually unnoticeable</td> </tr> </tbody></table> <p>The key observation: going from 16 to 8 bits halves your memory almost for free - the quality difference is unmeasurable on most tasks. The jump to 4 bits does start to show in careful, multi-step reasoning, but for ordinary chat, summarization, or code completion it’s perfectly sufficient.</p><h2 id="step-4-compare-outputs-on-the-same-prompt">Step 4: Compare outputs on the same prompt</h2> <p>Numbers are one thing, but reading the outputs side by side matters just as much. Give all three variants an identical, <strong>more demanding</strong> prompt - something with logic or precise facts, not just “write a poem”:</p><pre><code class="language-bash">for m in bf16 8bit 4bit; do echo "=== $m ===" mlx_lm.generate --model ./qwen3b-$m \ --prompt "I have 3 apples, I buy twice as many pears, and I eat 2 pieces of fruit. How many are left? Work through it step by step." \ --max-tokens 250 done </code></pre> <p>On simple tasks the outputs will be nearly identical. The more complex the reasoning, the more likely you’ll start seeing small stumbles in the 4-bit version - and that’s exactly where you decide which precision fits your use case.</p><h2 id="when-to-reach-for-which">When to reach for which</h2> <ul> <li><strong>4-bit</strong> - the default choice. Maximum capacity, smallest footprint, highest speed. Ideal for chat, RAG, code completion, and most agentic tasks.</li> <li><strong>8-bit</strong> - when you’re chasing quality on demanding reasoning or precise facts and memory allows. A lovely compromise.</li> <li><strong>bf16</strong> - reference quality, fine-tuning without compromises, or when you have memory to spare. Needlessly hungry for ordinary deployment.</li> </ul> <h2 id="whats-next">What’s next</h2> <p>You can now run a model and shrink it. In <strong>part 3</strong> we’ll finally adapt it to ourselves: we’ll fine-tune it on your own data with LoRA (and you’ll see that QLoRA is really fine-tuning running directly on top of that quantized model from this part) - all on a MacBook, no cloud.</p><hr> <p><em>Tested with <code>mlx-lm</code> 0.30.x on macOS 15+. Always check exact command flags with <code>--help</code>, as they occasionally change between versions.</em></p>

ai
Part 1: Run Your First Local LLM on a Mac in 10 Minutes
• <p><em>Series: AI on Apple Silicon — part 1 of 3</em></p><p>In the intro article we covered <strong>why</strong> a Mac with Apple Silicon is a great machine for local language models, and why MLX is the framework to reach for. Now let’s put it into practice. By the end of this part you’ll have a model running on your Mac that you can chat with in the terminal - and as a bonus, we’ll wire it into a code editor too.</p><p>The whole thing takes about ten minutes, most of which is downloading the model.</p><h2 id="what-youll-need">What you’ll need</h2> <ul> <li><strong>A Mac with Apple Silicon</strong> (M1 or newer). MLX doesn’t run on Intel.</li> <li><strong>macOS 15 or newer</strong> - required by the current <code>mlx-lm</code>.</li> <li><strong>A native arm64 Python</strong>, version 3.10–3.12. This is the most common stumbling block; we’ll come back to it shortly.</li> <li>A few gigabytes of disk space (a small 3B model at 4-bit takes around 2 GB).</li> </ul> <h2 id="step-1-set-up-your-environment">Step 1: Set up your environment</h2> <p>I recommend keeping every experiment in its own virtual environment. The quickest path is via <code>uv</code>, which pulls the correct arm64 Python build for you:</p><pre><code class="language-bash">uv venv --python 3.12 source .venv/bin/activate </code></pre> <p>If you don’t have <code>uv</code> and want to stick with the standard library:</p><pre><code class="language-bash">python3 -m venv .venv source .venv/bin/activate </code></pre> <p><strong>Why do we keep harping on arm64?</strong> If your Python accidentally runs under Rosetta (x86 emulation), MLX can’t reach the GPU and everything runs painfully slowly - or crashes. Verify it like this:</p><pre><code class="language-bash">python -c "import platform; print(platform.machine())" </code></pre> <p>It must print <code>arm64</code>. Once MLX is installed, the definitive test is:</p><pre><code class="language-bash">python -c "import mlx.core as mx; print(mx.metal.is_available())" </code></pre> <p>If you get <code>True</code>, you’re set - GPU acceleration via Metal is working.</p><h2 id="step-2-install-mlx-lm">Step 2: Install MLX LM</h2> <pre><code class="language-bash">pip install mlx-lm </code></pre> <p>That’s it. The <code>mlx-lm</code> package brings both the framework itself and the command-line tools <code>mlx_lm.generate</code>, <code>mlx_lm.chat</code>, <code>mlx_lm.convert</code>, and <code>mlx_lm.server</code>, which we’ll use throughout the series.</p><h2 id="step-3-your-first-generated-text">Step 3: Your first generated text</h2> <p>You don’t have to download anything manually - just point at a model on Hugging Face and <code>mlx-lm</code> will pull it into a local cache on first run:</p><pre><code class="language-bash">mlx_lm.generate \ --model mlx-community/Llama-3.2-3B-Instruct-4bit \ --prompt "Explain in two sentences what unified memory is." \ --max-tokens 200 </code></pre> <p>The first run takes a moment for the download (the model is stored in <code>~/.cache/huggingface/</code>). Every run after that starts in a few seconds.</p><p>The <code>mlx-community</code> organization on Hugging Face hosts thousands of pre-converted models, so you’ll always find a variant ready for Apple Silicon.</p><h2 id="step-4-chat-with-the-model">Step 4: Chat with the model</h2> <p>One-shot generation is fine for a test, but an interactive chat is more interesting:</p><pre><code class="language-bash">mlx_lm.chat --model mlx-community/Llama-3.2-3B-Instruct-4bit </code></pre> <p>This opens a REPL where you can type messages and the model keeps the conversation context. Exit with <code>Ctrl+C</code>.</p><p><strong>What just happened under the hood?</strong> On load, the model’s weights go into unified memory, which the CPU and GPU access at the same time - no copying across a bus, the way a discrete GPU would require. That’s why large models run so smoothly on a Mac, even when they’re “large” by laptop standards.</p><h2 id="step-5-bonus-wire-the-model-into-your-editor">Step 5 (bonus): Wire the model into your editor</h2> <p>This is where a toy turns into a tool. MLX LM can launch a local server with an OpenAI-compatible API:</p><pre><code class="language-bash">mlx_lm.server --model mlx-community/Mistral-7B-Instruct-v0.3-4bit </code></pre> <p>The server comes up on <code>localhost:8080</code> and exposes the <code>/v1/chat/completions</code> endpoint. Test it with a plain <code>curl</code>:</p><pre><code class="language-bash">curl localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"messages": [{"role": "user", "content": "Say this is a test."}]}' </code></pre> <p>Because the interface is OpenAI-compatible, you can drop it into most tools that support a “custom OpenAI endpoint” - Continue, Cline, or Zed, for example. In the config you just redirect the base URL to the local server:</p><pre><code class="language-json">{ "api_base": "http://localhost:8080/v1", "api_key": "local", "model": "mlx-lm" } </code></pre> <p>From this point on, your code completions run locally, for free, and privately - no token ever leaves your desk.</p><blockquote> <p>Note: the built-in server is great for development and personal use, but it isn’t built for production - it only has basic security checks.</p></blockquote> <h2 id="when-something-doesnt-work">When something doesn’t work</h2> <ul> <li><strong><code>GatedRepoError</code> during download</strong> - the model is gated. Log in with <code>huggingface-cli login</code> and accept the license on the model’s page.</li> <li><strong><code>mx.metal.is_available()</code> returns <code>False</code></strong> - you’re running under Rosetta or a non-native Python. Recreate the environment with an arm64 build (see Step 1).</li> <li><strong>The Mac is gasping or the process crashes</strong> - the model is too large for your RAM. Stick with a 4-bit variant of a smaller model. Rough guide: 3B at 4-bit takes about 2 GB, 7B about 4.5 GB.</li> </ul> <h2 id="picking-a-model-by-memory">Picking a model by memory</h2> <p>A rough rule of thumb for 4-bit models: on an 8 GB machine stay under 7B; 16 GB comfortably runs 8B (and even handles 14B); 32 GB and up opens the door to 30B+ models. The more unified memory, the bigger the model - that’s the whole magic of Apple Silicon.</p><h2 id="whats-next">What’s next</h2> <p>You’ve got a model running. In <strong>part 2</strong> we’ll look at quantization up close: we’ll take one model, convert it to 16, 8, and 4 bits, measure the difference in memory and speed, and see where the quality loss starts to show.</p><hr> <p><em>Tested with <code>mlx-lm</code> 0.30.x on macOS 15+. Always check exact command flags with <code>--help</code>, as they occasionally change between versions.</em></p>

ai
AI on macOS with Apple Silicon: Why It Pays to Run LLMs Through MLX
• <p>Over the past two years, Macs with Apple Silicon chips (M1 through M5) have gone from a curiosity to one of the most practical machines for running language models locally. The reason isn’t raw horsepower, the way it is with dedicated NVIDIA GPUs - it’s the architecture, and the software that takes full advantage of it. That software is <strong>MLX</strong>, an open-source framework from Apple Research.</p><h2 id="why-apple-silicon">Why Apple Silicon</h2> <p>The key advantage is the so-called <strong>unified memory</strong> architecture. On a typical PC with a dedicated graphics card, the model has to physically fit into the GPU’s VRAM - usually 8 to 24 GB on consumer hardware - and the model weights have to be copied back and forth across the PCIe bus. On Apple Silicon, the CPU and GPU share a single pool of memory at full bandwidth, so nothing gets copied anywhere.</p><p>In practice this means a MacBook with 64 GB of memory can run a 70-billion-parameter model at 4-bit quantization - a model that simply won’t fit into the 24 GB VRAM of a much more expensive gaming GPU. Capacity here is limited by the size of your RAM, not by a small amount of graphics memory, and for large models that’s a decisive difference.</p><h2 id="what-mlx-is">What MLX is</h2> <p>MLX is an array framework that Apple released in late 2023, built from the ground up for Apple Silicon. Its API closely follows NumPy (and the higher-level layers follow PyTorch), so it feels immediately familiar to people in the field. Under the hood it uses Metal for GPU acceleration and takes advantage of exactly that unified memory.</p><p>For language models there’s a layer called <strong>MLX LM</strong> - a package and set of command-line tools you install with a single <code>pip install mlx-lm</code>. It can pull thousands of models straight from Hugging Face (the <code>mlx-community</code> organization hosts around 4,800 already-converted models), start a chat with one command, generate text, and fine-tune a model directly on your own machine. MLX offers APIs for Python, Swift, C++, and C.</p><h2 id="why-use-the-optimized-variant">Why use the optimized variant</h2> <p>Running a “plain” model without Apple Silicon optimization means leaving a large chunk of performance on the table. MLX brings two things that are well worth it:</p><ul> <li><strong>Native quantization.</strong> Reducing the precision of the weights (for example to 4 bits) dramatically lowers the memory footprint. Converting a Hugging Face model with <code>mlx_lm.convert</code> takes only a few seconds.</li> <li><strong>Use of next-generation hardware.</strong> The M5 chip added so-called Neural Accelerators to its GPU cores - dedicated matrix-multiplication operations that are critical for inference. Apple reports up to a fourfold speedup in time-to-first-token compared to the M4.</li> </ul> <p>The ecosystem has also matured in other ways: the popular tool Ollama switched its Apple Silicon backend to one built on top of MLX, so even users who don’t know MLX directly benefit from it.</p><h2 id="summary">Summary</h2> <p>On a Mac with Apple Silicon, it makes sense to run LLMs through MLX because it leverages unified memory and Metal and supports both quantization and acceleration on the newest chips. The result is the ability to run models locally, privately, and offline - often at sizes that a standalone consumer GPU wouldn’t have the capacity to handle.</p>

vps
Discount coupons for WEDOS
• <p>Discount: 50%<br>Coupon code: <code>DM222GPGOZ</code><br>Services: <a href="https://www.wedos.cz/?ap=76802">domains</a> (.EU, .ONLINE, .STORE, .TECH, .SITE, .WEBSITE, .SPACE, .FUN), for the first year<br>Coupon valid until: 31.12.2022</p><p>Discount: 50%<br>Coupon code: <code>WN222KJZNB</code><br>Services: <a href="https://www.wedos.cz/?ap=76802">web hosting</a><br>Coupon valid until: 31.12.2022</p><p>Discount: 33%<br>Coupon code: <code>HVYNBPGEKU</code><br>Services: <a href="https://www.wedos.cz/?ap=76802">web hosting, vps, wedos disk</a><br>Coupon valid until: 08.12.2022<br><br>Discount: 33%<br>Coupon code: <code>N2LMA23XDM</code><br>Services: <a href="https://www.wedos.cz/?ap=76802">web hosting, vps, wedos disk</a><br>Coupon valid until: 08.12.2022</p><p>Discount: 25%<br>Coupon code: <code>VD222YPUZB</code><br>Services: <a href="https://www.wedos.cz/?ap=76802">vps ssd</a><br>Coupon valid until: 31.12.2022<br><br>All coupons can be used when ordering a service at <a href="https://www.wedos.cz/?ap=76802">WEDOS</a>. A non-working coupon = the coupon's usage limit has been reached.</p>

docker
Installing Pi-hole in Docker on an RPi4
• <h2>Installing docker on Raspbian OS</h2> <ul> <li><code>sudo apt-get update</code></li> <li><code>sudo apt-get install apt-transport-https ca-certificates curl gnupg lsb-release</code></li> <li><code>curl -sSL https://get.docker.com | sh</code></li> <li><code>sudo usermod -aG docker pi</code></li> <li><code>sudo systemctl enable docker</code></li> </ul> <h2>Installing docker-compose</h2> <ul> <li><code>sudo apt-get install libffi-dev libssl-dev</code></li> <li><code>sudo apt install python3-dev</code></li> <li><code>sudo apt-get install -y python3 python3-pip</code></li> <li><code>sudo pip3 install docker-compose</code></li> </ul> <h2>Installing Pi-hole</h2> <ul> <li>Create a docker compose file for the installation and save it as <code>docker-compose.yml</code></li> </ul> <pre><code class="language-YAML">version: "3" services: pihole: container_name: pihole image: pihole/pihole:latest ports: - "53:53/tcp" - "53:53/udp" - "67:67/udp" - "80:80/tcp" - "443:443/tcp" environment: TZ: 'Europe/Prague' volumes: - './etc-pihole/:/etc/pihole/' - './etc-dnsmasq.d/:/etc/dnsmasq.d/' dns: - 127.0.0.1 - 1.1.1.1 cap_add: - NET_ADMIN restart: unless-stopped </code></pre> <ul> <li>Start building the container with <code>sudo docker-compose up -d</code></li> <li>Connect into the newly created container <code>sudo docker exec -it pihole bash</code></li> <li>Change the password <code>pihole -a -p</code></li> <li>Pi-hole will be available on the server's IP on port 80 <ul> <li>for example <a href="http://192.168.1.55/admin">http://192.168.1.55/admin</a></li> </ul> </li> </ul> <p><strong>If we have, for example, a UFW firewall on the RPI, we also need to allow ports</strong> <code>53, 67, 80 and 443</code>.</p>

Guides
Securing a Raspberry Pi after installation
• <h2>Installing fail2ban</h2> <p>Fail2ban is software that helps prevent brute-force attacks and, after 5 failed login attempts, bans the IP for 10 minutes.</p> <ul> <li><code>sudo apt install fail2ban</code></li> <li><code>sudo service fail2ban restart</code></li> </ul> <h2>Installing a Firewall</h2> <p>On Linux antivirus isn't necessary, unless it's something like a NAS that's accessed from a Windows machine too. But it's very practical to install a firewall and set the rules correctly.</p> <ul> <li><code>sudo apt install ufw</code></li> <li>If we want to allow access to port 80, for example <ul> <li><code>sudo ufw allow 80</code></li> </ul> </li> <li>If we want to allow access to port 80 from a specific address <ul> <li><code>sudo ufw allow from 192.168.1.50 port 80</code></li> </ul> </li> <li>If we want to allow everything from a specific address <ul> <li><code>sudo ufw allow from 192.168.1.50</code></li> </ul> </li> <li>Before enabling the firewall it's good to have ssh access allowed, or we won't be able to get into the device without physical access <ul> <li><code>sudo ufw allow ssh</code></li> </ul> </li> <li>Enable the firewall with <code>sudo ufw enable</code></li> <li>We can check the rules with <code>sudo ufw status verbose</code></li> </ul> <p>The recommendation is to allow port 22, ssh connection, only from a specific address or subnet so that nobody from outside can log in over ssh.</p> <h2>Securing the SSH configuration</h2> <ul> <li>Open the SSH configuration <code>sudo vi /etc/ssh/sshd_config</code></li> <li>Find the line that says <code>PermitRootLogin prohibit-password</code> <ul> <li>If this line is present without <code>#</code>, add it to comment the line out and disable root login over ssh</li> </ul> </li> <li>The setting takes effect only after restarting the ssh service <code>sudo service ssh restart</code></li> </ul> <h2>Changing the password</h2> <ul> <li>After logging into the system, type <code>passwd</code> and change the password</li> </ul>

azure
Installing PowerShell on Linux and managing AzureAD and Exchange Online
• <p>If you want to manage Microsoft AD or Exchange from Linux, just write PowerShell scripts, or use PowerShell to help with various tasks on Linux, you can install it the official way on Debian Linux and "play around". This procedure was tested on Debian 11.</p> <h2>Procedure</h2> <ul> <li>Download the Microsoft repo GPG key <ul> <li><code>wget https://packages.microsoft.com/config/debian/10/packages-microsoft-prod.deb</code></li> </ul> </li> <li>Register the Microsoft repo GPG keys <ul> <li><code>sudo dpkg -i packages-microsoft-prod.deb</code></li> </ul> </li> <li>Update <ul> <li><code>sudo apt-get update</code></li> </ul> </li> <li>Install <ul> <li><code>sudo apt-get install -y powershell</code></li> </ul> </li> <li>Start the PS console <ul> <li><code>pwsh</code></li> </ul> </li> </ul> <h2>Installing modules for AzureAD and ExchangeOnline</h2> <ul> <li>register the repository for downloading modules <ul> <li><code>Register-PackageSource -Trusted -ProviderName 'PowerShellGet' -Name 'Posh Test Gallery' -Location https://www.poshtestgallery.com/api/v2/</code></li> <li><code>Register-PackageSource -Trusted -ProviderName 'PSGallery' -Name 'PS Gallery' -Location https://www.powershellgallery.com/api/v2/</code></li> </ul> </li> <li>Install the AzureAD module <ul> <li><code>Install-Module AzureAD.Standard.Preview</code></li> </ul> </li> <li>Install the Exchange Online module <ul> <li><code>Install-Module ExchangeOnlineManagement</code></li> </ul> </li> <li>Install WSMAN, without which login won't work due to a missing SSL library <ul> <li><code>Install-Module -Name PSWSMan -Scope AllUsers</code></li> <li><code>Install-WSMan</code></li> </ul> </li> </ul> <h2>Connecting to Azure AD</h2> <p><a href="https://docs.microsoft.com/en-us/powershell/module/?view=azureadps-2.0">AzureAD module - command set</a></p> <ul> <li> <p>Enter the login command for AzureAD</p> <ul> <li><code>Connect-AzureAd</code><br> <img src="https://rn.itmoov.eu/media/posts/22/powershell_na_linuxu_connect_azure_01.png" alt="powershell_na_linuxu_connect_azure_01" loading="lazy"></li> </ul> </li> <li> <p>It shows that we should click through to the browser using the attached link and enter the generated code there - so we do<br> <img src="https://rn.itmoov.eu/media/posts/22/powershell_na_linuxu_connect_azure_02.png" alt="powershell_na_linuxu_connect_azure_02" loading="lazy"></p> <p><img src="https://rn.itmoov.eu/media/posts/22/powershell_na_linuxu_connect_azure_03.png" alt="powershell_na_linuxu_connect_azure_03" loading="lazy"></p> <p><img src="https://rn.itmoov.eu/media/posts/22/powershell_na_linuxu_connect_azure_04.png" alt="powershell_na_linuxu_connect_azure_04" loading="lazy"></p> </li> <li> <p>Then the connection is verified and we can return to the console where we'll already be logged in<br> <img src="https://rn.itmoov.eu/media/posts/22/powershell_na_linuxu_connect_azure_05.png" alt="powershell_na_linuxu_connect_azure_05" loading="lazy"></p> </li> <li> <p>To sign out, enter the command</p> <ul> <li><code>Disconnect-AzureAd</code></li> </ul> </li> </ul> <h2>Connecting to Exchange Online</h2> <p><a href="https://docs.microsoft.com/en-us/powershell/module/exchange/?view=exchange-ps">ExchangeOnline - command set</a></p> <ul> <li>Enter the login command for Exchange Online <ul> <li><code>Connect-ExchangeOnline</code><br> <img src="https://rn.itmoov.eu/media/posts/22/powershell_na_linuxu_connect_sharepoint_01.png" alt="powershell_na_linuxu_connect_sharepoint_01" loading="lazy"></li> </ul> </li> <li>It shows that a browser window opened where you need to log in with an MS account and confirm the login<br> <img src="https://rn.itmoov.eu/media/posts/22/powershell_na_linuxu_connect_sharepoint_02.png" alt="powershell_na_linuxu_connect_sharepoint_02" loading="lazy"></li> <li>Then the connection is verified and we can return to the console where we'll already be logged in<br> <img src="https://rn.itmoov.eu/media/posts/22/powershell_na_linuxu_connect_sharepoint_03.png" alt="powershell_na_linuxu_connect_sharepoint_03" loading="lazy"></li> <li>To sign out, enter the command <ul> <li><code>Disconnect-ExchangeOnline</code></li> <li>Without signing out we'd needlessly waste sessions, of which there's a limited number - exactly 3! Sessions are restored after they expire over time; until then we can't reconnect</li> </ul> </li> </ul>

apache
Let's Encrypt with automatic renewal
• <p>Since restrictions on using sites without https - i.e. without a certificate - keep growing, we'll issue such a certificate for our site and also set up automatic renewal. We'll configure this on a reverse nginx proxy, but the procedure will be quite similar when configuring directly on individual sites.</p> <h2>Installing and generating the certificate</h2> <ul> <li>Run apt update <ul> <li><code>apt-get update</code></li> </ul> </li> <li>Install certbot, which will take care of the certificates <ul> <li><code>apt-get install certbot -y</code></li> </ul> </li> <li>Install the package that gives certbot additional features <ul> <li><code>apt-get install python-certbot-nginx -y</code></li> </ul> </li> <li>Create a certificate for the desired site (assuming the nginx proxy already contains some sites) <ul> <li><code>certbot --nginx -d domena.cz -d www.domena.cz</code></li> <li>enter an email</li> <li>accept the license agreement - <code>Y</code></li> <li>decline sharing our email address - <code>N</code></li> <li>choose whether to redirect http to https (<code>2</code>) or not (<code>1</code>)</li> </ul> </li> <li>After entering, it should show that the certificate was successfully created and where it was saved</li> <li>If we open that site's nginx configuration, we'll see that the necessary configuration was added automatically (if we chose option <code>2</code>) for HTTPS, with the certificate locations and the http-to-https redirect</li> </ul> <h2>Automatic certificate renewal</h2> <p>A Let's Encrypt certificate is valid for only 90 days, which means we'd have to log into the server every 90 days and manually generate a new certificate. But we can make it easier in this simple way using <a href="https://crontab.tech/">cron</a>. The line entered in the step below means that every day at midnight a certificate renew command runs; certbot then renews the certificate if its validity is less than 30 days.</p> <p>If we don't have cron on Linux, install it with <code>apt-get install cron -y</code></p> <ul> <li>Open the crontab for editing <ul> <li><code>crontab -e</code></li> <li>Start editing using the <code>i</code> or <code>insert</code> key</li> <li>Enter the following line <code>0 12 * * * /usr/bin/certbot renew --quiet</code></li> <li>Exit editing using the <code>ESC</code> key</li> <li>Save the crontab changes by typing <code>:wq!</code></li> </ul> </li> <li>If we want to verify the setting, enter <code>crontab -l</code></li> </ul>