
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>

dns
Email on your own domain using seznam.cz
• <p>We'll show how to quickly and easily set up email for your own domain without needing your own server. We'll use the seznam.cz service and create such an email server with them. Best of all, it's completely free.</p> <p>The guide assumes you already own a domain, but if not you can buy one for example at <a href="https://www.wedos.cz/domeny?ap=76802">WEDOS</a> and with this coupon <code>DM212HUZIU</code> you get 50% off the domain.</p> <h2>Procedure</h2> <ul> <li>Go to <a href="https://emailprofi.seznam.cz">emailprofi.seznam.cz</a></li> <li>Sign in with a Seznam account <ul> <li>the account must be @seznam.cz or @email.cz</li> </ul> </li> <li>After signing in, a page opens with a large orange button <code>Add new domain</code></li> <li>After clicking, a page opens where we enter our domain <ul> <li>for example <code>novak.cz</code></li> </ul> </li> <li>Choose that we have our own domain and continue</li> <li>On the next page, two important values appear; these connect your domain with the Seznam email server via DNS <ul> <li>first, on the purchased domain, delete the existing MX records in the DNS settings</li> <li>then create the first MX record <ul> <li>Name: empty</li> <li>TTL: <code>300</code></li> <li>Type: <code>MX</code></li> <li>Data: <code>10 <value generated by Seznam>.mx1.emailprofi.seznam.cz</code></li> </ul> </li> <li>Once the first is saved, do the same with the second, just use 20 instead of 10</li> <li>Apply the DNS changes</li> </ul> </li> <li>Finish the wizard in the Seznam administration and return to the <a href="https://emailprofi.seznam.cz">administration</a> <ul> <li>Domain verification can take up to 48 hours, but it's usually verified within an hour</li> </ul> </li> <li>Once the domain is verified, we can create a mailbox for the added domain <ul> <li>Click <code>Manage this organization</code></li> <li>Then click <code>Create first mailbox</code></li> <li>Enter the basic mailbox details and click <code>Continue</code></li> </ul> </li> <li>After the mailbox is created, a success message appears and we can sign in to the mailbox</li> </ul>

apache
AreWeDown? Installing on Docker
• <p>AreWeDown is a simple application/server monitoring tool that can send alerts, or you can leave it displayed on a screen to watch how online your services are. We'll show how to get such simple monitoring running in Portainer in a docker container.</p> <h2>Installation</h2> <ul> <li>First we need to create a volume where the configuration will be stored <ul> <li>In Portainer, click <code>Volumes</code> in the left menu</li> <li>In the next window click <code>Add Volume</code></li> <li>Name it for example <code>AreWD</code> and confirm with the <code>Create the volume</code> button</li> </ul> </li> <li>Next we need to create a container <ul> <li>In Portainer, click <code>Containers</code> in the left menu</li> <li>In the next window click <code>Add container</code></li> <li>Enter a container name, for example <code>arewedown</code></li> <li>As the image enter <code>shukriadams/arewedown:0.2.5</code></li> <li>In the <code>Network ports configuration</code> section, manually add one port (any free <strong>host</strong> port can be chosen) <ul> <li>host: <code>81</code> -> container: <code>3000</code></li> </ul> </li> <li>In the <code>Advanced container settings</code> section, expand <code>Volumes</code> and add a mapping <ul> <li>container: <code>/etc/arewedown/config</code> -> volume: <code>areWD - local</code></li> </ul> </li> <li>Now click <code>Deploy the container</code></li> </ul> </li> </ul> <h2>Configuring monitoring</h2> <ul> <li>On the host system, find the created docker volume so we can edit the arewedown configuration <ul> <li>the most common location is <code>/var/lib/docker/volumes/</code></li> <li>in our case it will be <code>/var/lib/docker/volumes/areWD/_data/</code></li> </ul> </li> <li>Open the configuration (if it's missing entirely, create the file -> <code>touch settings.yaml</code>)</li> <li>Into the configuration add, as an example, monitoring of one website and one ts3 server</li> </ul> <pre><code>header: Uptime watchers: itmooveu: # checks if this website is up interval: "*/2 * * * *" url: http://rn.itmoov.eu ts3: test: net.portOpen host: 192.168.0.10 port: 10011 </code></pre> <ul> <li>The <code>header</code> parameter sets the header of the web page</li> <li><code>interval</code> determines, using cron syntax, how often the check runs</li> </ul> <figure class="kg-card kg-image-card"><figure class="kg-image"><img src="https://rn.itmoov.eu/media/posts/19/Sn-mek-obrazovky-2021-07-19-v-11.22.06.png" alt loading="lazy" width="2000" height="717"></figure></figure>

apache
Basic Nginx proxy
• <p>Nginx is an open-source software web server with load management and a reverse proxy. It works with HTTP, SMTP, POP3, IMAP and SSL protocols. It focuses mainly on high performance and low memory usage. That's the definition from Wikipedia. In practice nginx is used mainly as a proxy server that routes websites to the correct addresses and ports and optionally covers them with an SSL certificate. Today we'll show the basic configuration of such a proxy. The guide focuses mainly on the configuration, which was done in a docker container, but it will be the same when installing on bare Linux.</p> <p><strong>Fictional sites we'll enter into the proxy:</strong></p> <ul> <li>domena1.cz</li> <li>domena2.cz</li> <li>domena3.cz</li> </ul> <p><strong>Fictional servers running apache2 with the sites for the domains:</strong></p> <ul> <li>www1 and www2 - 192.168.1.10</li> <li>www3 - 192.168.1.11</li> </ul> <p><strong>Ports of the fictional sites:</strong></p> <ul> <li>www1 - 8080</li> <li>www2 - 8081</li> <li>www3 - 8080</li> </ul> <p><strong>Server running the nginx proxy:</strong></p> <ul> <li>192.168.1.2</li> </ul> <p><strong>Nginx proxy ports:</strong></p> <ul> <li>80</li> <li>443</li> </ul> <h2>Configuring the nginx proxy</h2> <ul> <li>Open <code>/etc/nginx/nginx.conf</code></li> <li>In the http section, add a parameter that increases the allowed number of characters in the domain entered as server_name <ul> <li><code>server_names_hash_bucket_size 128;</code></li> </ul> </li> <li>Save this change and close the file</li> <li>Move to <code>/etc/nginx/conf.d/</code> and create a file <code>proxy.conf</code> here</li> <li>Open the newly created configuration file and enter the following <ul> <li> <pre><code class="language-bash:">server { listen 80; listen [::]:80; server_name domena1.cz; location / { proxy_pass http://192.168.1.10:8080; } } server { listen 80; listen [::]:80; server_name domena2.cz; location / { proxy_pass http://192.168.1.10:8081; } } server { listen 80; listen [::]:80; server_name domena3.cz; location / { proxy_pass http://192.168.1.11:8080; } } </code></pre> </li> </ul> </li> <li>Save and close the file</li> <li>Now reload the nginx service so it picks up the new configuration file and applies the changes <ul> <li><code>service nginx reload</code></li> </ul> </li> </ul> <h2>Configuring DNS</h2> <p>Now we want the domains to point to the proxy server, which then routes them on to the correct apache servers and their ports, so we need to set DNS for the domains.</p> <ul> <li>Open the DNS settings for the domain domena1.cz (this procedure can be replicated for the remaining domains)</li> <li>Choose a new A record and fill it in correctly <ul> <li><strong>Name:</strong> domena1.cz</li> <li><strong>TTL:</strong> 300</li> <li><strong>Type:</strong> A</li> <li><strong>Value:</strong> 192.168.1.2</li> </ul> </li> <li>Save the record and let the changes apply - these usually propagate within an hour for a public DNS provider</li> </ul> <p>Now when you enter <a href="http://domena1.cz">http://domena1.cz</a> in your browser, the flow will be:</p> <ul> <li><code>http://domena1.cz -> 192.168.1.2:80 -> 192.168.1.10:8080</code></li> </ul>