<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sergej Voronko</title>
    <description>The latest articles on DEV Community by Sergej Voronko (@linuxcore).</description>
    <link>https://dev.to/linuxcore</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4195468%2Fcaa1ff11-44a2-4c36-bc32-9207c04edc14.png</url>
      <title>DEV Community: Sergej Voronko</title>
      <link>https://dev.to/linuxcore</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/linuxcore"/>
    <language>en</language>
    <item>
      <title>Fail2ban vs CrowdSec: Homelab IPS Battle</title>
      <dc:creator>Sergej Voronko</dc:creator>
      <pubDate>Fri, 30 Oct 2026 23:42:02 +0000</pubDate>
      <link>https://dev.to/linuxcore/fail2ban-vs-crowdsec-homelab-ips-battle-j1f</link>
      <guid>https://dev.to/linuxcore/fail2ban-vs-crowdsec-homelab-ips-battle-j1f</guid>
      <description>&lt;p&gt;When evaluating fail2ban vs crowdsec for a homelab IPS, the choice replaces a local regex log scanner with a distributed consensus engine. While Fail2ban parses text logs and directly appends blocking rules to your firewall, CrowdSec separates the workload into a log parser, a Local API, and distinct remediation components (commonly known as CrowdSec bouncers). For a homelab administrator, the decision comes down to whether the immediate simplicity of a single Python script outweighs the performance and shared threat intelligence of a decoupled architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Fail2ban Processes Logs and Iptables Rules
&lt;/h2&gt;

&lt;p&gt;Fail2ban is the default intrusion prevention system for most self-hosted servers. The service is a Python daemon that runs regular expressions against system log files. When an IP address matches a failure condition too many times within a specific &lt;code&gt;findtime&lt;/code&gt; window, Fail2ban triggers an action. That action usually involves appending an IP block to an &lt;code&gt;iptables&lt;/code&gt; or &lt;code&gt;nftables&lt;/code&gt; chain on the local machine.&lt;/p&gt;

&lt;p&gt;The implementation requires zero external dependencies. You define a filter file containing a regex, link it to a jail in &lt;code&gt;jail.local&lt;/code&gt;, specify a ban duration, and start the service.&lt;/p&gt;

&lt;p&gt;The limitations appear under load. Regular expression evaluation is notoriously CPU-intensive. If a homelab exposes a public application that suffers an aggressive brute-force attack or a rapid vulnerability scan, the sudden influx of log entries forces the Python daemon to constantly evaluate complex regex rules against a fast-moving stream of text. Server CPU usage frequently spikes during these events.&lt;/p&gt;

&lt;p&gt;Relying on vanilla &lt;code&gt;iptables&lt;/code&gt; rules to drop connections from thousands of IP addresses forces the Linux kernel to perform linear searches. Unless Fail2ban is specifically configured to use an &lt;code&gt;ipset&lt;/code&gt; action, a large ban list severely degrades packet forwarding performance. A large list of banned IPs means every valid incoming packet must be checked against those sequential firewall rules before reaching its destination.&lt;/p&gt;

&lt;p&gt;Fail2ban also stores its active bans in a local SQLite database located at &lt;code&gt;/var/lib/fail2ban/fail2ban.sqlite3&lt;/code&gt;. If the &lt;code&gt;dbpurgeage&lt;/code&gt; parameter is not tuned correctly, this database grows massive over time and slows down service restarts.&lt;/p&gt;

&lt;h2&gt;
  
  
  CrowdSec Architecture and Bouncer Design
&lt;/h2&gt;

&lt;p&gt;CrowdSec is written in Go and fundamentally changes the intrusion prevention model. Instead of one monolithic script handling parsing, state management, and blocking, the architecture is split into discrete services.&lt;/p&gt;

&lt;p&gt;The core agent uses YAML configurations and Grok patterns to parse logs. Grok patterns execute much faster than raw regex evaluations in Python, reducing CPU overhead during high-volume attacks.&lt;/p&gt;

&lt;p&gt;When the agent detects malicious behavior based on defined scenarios, it does not touch the host firewall directly. Instead, it registers a decision with the Local API (LAPI), which stores the event in a local SQLite or PostgreSQL database.&lt;/p&gt;

&lt;p&gt;Remediation components (the CrowdSec bouncers) query this LAPI. The bouncer is responsible for enforcing the ban. If you install the host firewall bouncer, it creates an &lt;code&gt;ipset&lt;/code&gt; or &lt;code&gt;nftables&lt;/code&gt; set and adds the malicious IP address to it for fast lookups. Because the components communicate over an HTTP API, you can run the parsing agent on an internal web server and run the bouncer on your perimeter router.&lt;/p&gt;

&lt;p&gt;This architecture includes a community threat intelligence feature. The LAPI periodically sends anonymous data about banned IPs to a Central API. In return, your homelab receives a curated blocklist of IPs that have attacked other CrowdSec users. Your server drops packets from known malicious actors before they ever attempt a connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying CrowdSec via Docker Compose
&lt;/h2&gt;

&lt;p&gt;Deploying Fail2ban in Docker often requires elevated privileges and messy host file mounts. CrowdSec is designed with containers in mind. The agent runs as a standard unprivileged container, and you configure your reverse proxy to share its log directory.&lt;/p&gt;

&lt;p&gt;Below is a minimal Docker Compose snippet based on the current official CrowdSec documentation. The configuration mounts the host log directory, exposes the Local API on port 8080, and installs common collections.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;crowdsec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crowdsecurity/crowdsec:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crowdsec&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;COLLECTIONS=crowdsecurity/linux crowdsecurity/traefik crowdsecurity/http-cve&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GID=1000&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;crowdsec-db:/var/lib/crowdsec/data/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;crowdsec-config:/etc/crowdsec/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/log/auth.log:/var/log/auth.log:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/path/to/proxy/logs:/var/log/proxy:ro&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;crowdsec-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;crowdsec-config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;COLLECTIONS&lt;/code&gt; environment variable automatically downloads parsers and scenarios from the CrowdSec Hub. A collection is a bundled set of parsing rules and detection thresholds for a specific service. The Local API binds to &lt;code&gt;127.0.0.1&lt;/code&gt; so that host-level bouncers can query it securely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting CrowdSec Bouncers to the LAPI
&lt;/h2&gt;

&lt;p&gt;Running the CrowdSec agent alone only populates a database with decisions; it does not block anything. You must install a bouncer to apply the remediations. &lt;/p&gt;

&lt;p&gt;Bouncers come in two primary categories: Infrastructure-level (Layer 3/4) and Application-level (Layer 7). &lt;br&gt;
The infrastructure bouncer (&lt;code&gt;crowdsec-firewall-bouncer&lt;/code&gt;) runs directly on the host operating system and issues kernel-level drops. &lt;br&gt;
The application bouncers integrate directly into reverse proxies like Traefik, Caddy, or Nginx. Instead of dropping the packet at the kernel, a Layer 7 bouncer can return a 403 Forbidden HTTP error or serve a CAPTCHA challenge to suspicious IPs.&lt;/p&gt;

&lt;p&gt;To connect a bouncer, you must generate an API token using the &lt;code&gt;cscli&lt;/code&gt; command line tool inside the CrowdSec container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;crowdsec cscli bouncers add homelab-firewall
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command outputs an alphanumeric token. You paste this token into the bouncer's configuration file and restart the bouncer service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Homelab Gotchas: WAL Mode and Docker Networking
&lt;/h2&gt;

&lt;p&gt;The distributed design introduces management complexity. Moving from Fail2ban to CrowdSec requires learning a new taxonomy of agents, scenarios, parsers, and bouncers. You will rely heavily on the &lt;code&gt;cscli&lt;/code&gt; tool to inspect decisions and manage collections.&lt;/p&gt;

&lt;p&gt;The SQLite database lock issue is a documented gotcha for high-traffic homelabs. By default, the CrowdSec container initializes a standard SQLite database. When multiple logs are parsed rapidly and the bouncer pulls updates simultaneously, the database can throw a "database is locked" error in the container logs. The fix requires manually adding &lt;code&gt;use_wal: true&lt;/code&gt; to the &lt;code&gt;db_config&lt;/code&gt; section of &lt;code&gt;/etc/crowdsec/config.yaml&lt;/code&gt; to enable Write-Ahead Logging, followed by a container restart.&lt;/p&gt;

&lt;p&gt;Docker networking creates another frequent issue with bouncers. If you run a bouncer in a separate Docker container on a custom bridge network, it communicates with the LAPI via a dynamic container IP address. If the bouncer container updates or restarts, Docker assigns it a new internal IP. The CrowdSec LAPI sees a new connection and leaves the old bouncer registered. Over time, stale bouncer registrations accumulate. Administrators must implement static container IPs or prune inactive bouncers periodically using the &lt;code&gt;cscli bouncers prune&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;Conversely, Fail2ban requires almost no maintenance once the jail is dialed in. If you have a single virtual private server or a single home server running SSH, Fail2ban is highly effective. You install the package, enable the &lt;code&gt;sshd&lt;/code&gt; jail, and the configuration survives system reboots and package updates without demanding attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Stick With Fail2ban vs CrowdSec
&lt;/h2&gt;

&lt;p&gt;A homelab running a simple WireGuard endpoint and SSH does not require CrowdSec. Fail2ban provides immediate protection with minimal setup. The CPU penalty of regex parsing is irrelevant when your public attack surface is restricted to a single port and traffic volume is low.&lt;/p&gt;

&lt;p&gt;Homelabs hosting multiple public-facing web applications behind a reverse proxy gain significant advantages from CrowdSec. The Go-based log parsing handles heavy traffic without spiking processor usage. The community threat intelligence preemptively drops known malicious scanners, reducing the sheer volume of traffic hitting your applications. Most importantly, the bouncers allow you to decouple detection from enforcement, parsing proxy logs safely inside an isolated container while executing the ban via the host network stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/homelab/linux-security-hardening/" rel="noopener noreferrer"&gt;Linux Homelab Security Hardening: Complete Checklist (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/homelab/docker-compose-homelab-stack/" rel="noopener noreferrer"&gt;Docker Compose Homelab Stack: 20 Services, One File&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/homelab/self-host-static-ip-living-room-homelab/" rel="noopener noreferrer"&gt;Self-Hosting on a Static IP: The No-Nonsense Homelab Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the main difference between Fail2ban and CrowdSec?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fail2ban uses local regex log parsing to update iptables directly. CrowdSec uses a decoupled architecture with Go-based grok parsing and a distributed threat intelligence network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can Fail2ban and CrowdSec run together?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Running both on the same logs creates duplicate firewall rules and race conditions. Pick one intrusion prevention system per service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do CrowdSec bouncers cost money?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The core CrowdSec engine and standard bouncers are free and open-source. The project limits API pulls for enterprise volume, but standard homelab usage remains free.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://linuxcore.dev/homelab/fail2ban-vs-crowdsec-homelab-firewall/" rel="noopener noreferrer"&gt;linuxcore.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>fail2ban</category>
      <category>crowdsec</category>
      <category>homelab</category>
    </item>
    <item>
      <title>Astro MD vs MDX: Content Collection Build Performance</title>
      <dc:creator>Sergej Voronko</dc:creator>
      <pubDate>Wed, 28 Oct 2026 23:42:02 +0000</pubDate>
      <link>https://dev.to/linuxcore/astro-md-vs-mdx-content-collection-build-performance-1m9p</link>
      <guid>https://dev.to/linuxcore/astro-md-vs-mdx-content-collection-build-performance-1m9p</guid>
      <description>&lt;p&gt;Standard Markdown (&lt;code&gt;.md&lt;/code&gt;) and MDX (&lt;code&gt;.mdx&lt;/code&gt;) process content drastically differently within Astro content collections. The build pipeline parses standard Markdown as raw text data, requiring minimal memory and scaling linearly. The build process compiles MDX files into full JSX components, forcing the Vite bundler to process every &lt;code&gt;.mdx&lt;/code&gt; file as a discrete JavaScript module. This architectural difference means MDX processing introduces significant parser overhead and increases build times on low-resource homelab CI runners. Use standard Markdown for static text and reserve MDX strictly for files requiring embedded interactive components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Astro Markdown vs MDX: Parser Architecture
&lt;/h2&gt;

&lt;p&gt;Standard &lt;code&gt;.md&lt;/code&gt; files in Astro bypass the JavaScript module graph entirely. When the content collections API processes a &lt;code&gt;.md&lt;/code&gt; file, the parser reads the frontmatter for validation against your defined Zod schema. The standard remark and rehype plugins then transform the body text into HTML. The default Astro pipeline relies on a Rust-powered processor that significantly improves build times. The content remains isolated from the application's core JavaScript bundle.&lt;/p&gt;

&lt;p&gt;MDX skips lightweight text processing. When you install the &lt;code&gt;@astrojs/mdx&lt;/code&gt; package, the compiler evaluates every &lt;code&gt;.mdx&lt;/code&gt; file as a functional component. The parser transforms your Markdown syntax into JSX, injects imports for any Astro, Vue, or React components referenced in the file, and hands the resulting code to Vite. The pipeline then adds these modules to the overall dependency graph.&lt;/p&gt;

&lt;p&gt;This fundamental shift alters how the server builds the site. A basic Markdown file maps directly to static HTML output. An MDX file requires the build system to resolve imports, manage potential hydration directives, and pass the output through the Vite build pipeline before generating that same static HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build Speeds and Parser Overhead
&lt;/h2&gt;

&lt;p&gt;If you run builds on a minimal Proxmox virtual machine or a cheap VPS, the performance delta between &lt;code&gt;.md&lt;/code&gt; and &lt;code&gt;.mdx&lt;/code&gt; becomes a hard limit. A content collection containing 1,000 standard Markdown files compiles in under a few seconds. Converting those same 1,000 files to MDX pushes the build time into the double digits and routinely triggers out-of-memory kills on runners with under 2GB of RAM.&lt;/p&gt;

&lt;p&gt;The performance drop stems from the compilation pipeline. When the build pipeline encounters an MDX file, it does not just render text. The &lt;code&gt;@mdx-js/mdx&lt;/code&gt; compiler parses the document into a Markdown Abstract Syntax Tree (AST), applies any configured remark plugins, and converts the output to an HTML AST. The compiler then applies rehype plugins and maps the resulting structure into raw JSX syntax. Finally, the process passes this JSX to the internal bundler to transpile into standard JavaScript. This entire chain executes for every single MDX file. Processing 2,000 blog posts requires 2,000 distinct JavaScript module compilations.&lt;/p&gt;

&lt;p&gt;When a homelab CI runner hits the default Node.js heap limit during an Astro build, the pipeline crashes. You can temporarily bypass the crash by forcing Node to use more memory, but this is a patch, not a fix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Expanding the heap size for heavy MDX builds&lt;/span&gt;
&lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--max-old-space-size=4096"&lt;/span&gt; npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proper fix is limiting MDX usage to files that actually require it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Component Architecture and Hydration
&lt;/h2&gt;

&lt;p&gt;Standard Markdown restricts you to standard HTML tags. You can apply custom styling via CSS, but you cannot embed stateful client-side code directly into the prose. &lt;/p&gt;

&lt;p&gt;Many developers assume they need MDX just to add custom CSS classes to alerts or styled callouts. You do not need MDX for basic structural changes. You can write or import custom remark and rehype plugins for standard Markdown that automatically inject HTML &lt;code&gt;div&lt;/code&gt; wrappers or CSS classes around standard blockquotes. Standard Markdown in Astro also utilizes Shiki for syntax highlighting by default, meaning you do not need MDX just to render formatted code blocks.&lt;/p&gt;

&lt;p&gt;MDX exists specifically to import UI components directly into the content body. This is necessary when an article requires a functional, interactive element, like a live search bar, a dynamic data table fetched from an API, or a React component tracking client-side state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
title: "Server Status"
pubDate: 2026-10-27
---

import PingTest from '../../components/PingTest.jsx';

The current server latency is listed below. Click the button to refresh the ping.

&amp;lt;PingTest client:load target="192.168.1.100" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because Astro uses an Islands architecture, the MDX file itself still renders to static HTML. Client-side JavaScript only ships to the browser if you explicitly append a client directive like &lt;code&gt;client:load&lt;/code&gt; or &lt;code&gt;client:visible&lt;/code&gt; to the embedded component. Without the directive, the component renders strictly on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing MDX in Astro Content Collections
&lt;/h2&gt;

&lt;p&gt;If your project requires embedded islands, you must explicitly enable MDX integration and configure your content loader to recognize the extension. &lt;/p&gt;

&lt;p&gt;Install the official integration using the Astro CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx astro add mdx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optimize the &lt;code&gt;@astrojs/mdx&lt;/code&gt; integration in your configuration file. The &lt;code&gt;optimize&lt;/code&gt; flag instructs the internal rehype plugin to generate a smaller abstract syntax tree, which cuts down build times and reduces memory consumption for large collections. If you do not require standard Markdown configuration settings to apply to your MDX files, setting &lt;code&gt;extendMarkdownConfig&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; ignores inherited project-level Markdown options entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// astro.config.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mdx&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@astrojs/mdx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;mdx&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;optimize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;extendMarkdownConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, configure your content collection loader to target both file types. Astro uses the &lt;code&gt;glob&lt;/code&gt; loader for local file fetching. You can mix &lt;code&gt;.md&lt;/code&gt; and &lt;code&gt;.mdx&lt;/code&gt; files in the same directory by defining a multi-extension pattern. The &lt;code&gt;[^_]&lt;/code&gt; regex ensures Astro ignores files prefixed with an underscore.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/content/config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineCollection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro:content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;glob&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro/loaders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;docsCollection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineCollection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/[^_]*.{md,mdx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/content/docs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;pubDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;collections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;docsCollection&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When rendering the collection in a dynamic route, the &lt;code&gt;render()&lt;/code&gt; function parses both file types automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/pages/docs/[...slug].astro
import { getCollection, render } from 'astro:content';

export async function getStaticPaths() {
  const docs = await getCollection('docs');
  return docs.map(entry =&amp;gt; ({
    params: { slug: entry.id },
    props: { entry },
  }));
}

const { entry } = Astro.props;
const { Content } = await render(entry);
---

&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;{entry.data.title}&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;article&amp;gt;
      &amp;lt;Content /&amp;gt;
    &amp;lt;/article&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When to Choose Raw Astro Markdown
&lt;/h2&gt;

&lt;p&gt;Stick to &lt;code&gt;.md&lt;/code&gt; files if your content consists only of headings, paragraphs, standard code blocks, and images. The native Astro content collections API handles all frontmatter validation and basic HTML rendering automatically. &lt;/p&gt;

&lt;p&gt;Standard Markdown formatting keeps your content portable. If you migrate away from Astro in the future, raw Markdown transfers cleanly to Hugo, Jekyll, or any other static site generator. MDX files littered with framework-specific component imports require manual clean-up and regex replacements to become standard text again. &lt;/p&gt;

&lt;p&gt;Integrating a headless CMS like Decap or Ghost is much simpler with standard Markdown. Remote content management systems typically export plain text or JSON. Forcing remote content into an MDX pipeline requires custom loaders and complex hydration strategies that break the native Astro content flow.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;.mdx&lt;/code&gt; exclusively when a specific document requires interactivity that standard Markdown cannot provide. You can safely mix &lt;code&gt;.md&lt;/code&gt; and &lt;code&gt;.mdx&lt;/code&gt; within the same Astro content collection. Keep the bulk of your site as fast-compiling Markdown, and selectively upgrade individual files to MDX when the content demands custom React, Vue, or Svelte components. This split approach maintains fast build times on limited CI runners while preserving the ability to embed complex UI elements.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/astro/astro-7-4-build-timings-parallel-prerendering-guide/" rel="noopener noreferrer"&gt;Astro 7.4 Build Timings &amp;amp; Parallel Prerendering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/astro/astro-7-alpha-vite-v8-upgrade-what-to-expect/" rel="noopener noreferrer"&gt;Astro 7 and Vite 8: What Changes When You Upgrade&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/astro/nextjs-to-astro-5-migration-homelab-cost-performance/" rel="noopener noreferrer"&gt;Migrating Next.js to Astro: What Changes and Why&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I mix Markdown and MDX files in the same Astro content collection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The glob loader accepts multiple extensions. Configure the pattern to '*&lt;em&gt;/[^_]&lt;/em&gt;.{md,mdx}' to process both file types in a single collection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does MDX ship JavaScript to the browser by default?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. MDX compiles to HTML at build time. Client-side JavaScript only ships if you append a client directive like client:load to the embedded framework component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why are my Astro MDX builds failing with out-of-memory errors?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vite bundles every MDX file as a discrete JavaScript module. Large MDX collections overwhelm CI runners with limited RAM. Enable the optimize flag in the MDX integration or increase runner memory.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://linuxcore.dev/astro/astro-md-vs-mdx-performance-guide/" rel="noopener noreferrer"&gt;linuxcore.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>astro</category>
      <category>mdx</category>
      <category>markdown</category>
    </item>
    <item>
      <title>Expose Share Links on Private Homelab Apps With Caddy</title>
      <dc:creator>Sergej Voronko</dc:creator>
      <pubDate>Fri, 23 Oct 2026 22:42:02 +0000</pubDate>
      <link>https://dev.to/linuxcore/expose-share-links-on-private-homelab-apps-with-caddy-4fmo</link>
      <guid>https://dev.to/linuxcore/expose-share-links-on-private-homelab-apps-with-caddy-4fmo</guid>
      <description>&lt;p&gt;Homelab administrators often lock down self-hosted applications using an identity provider like Authelia or Authentik. This creates a hard authentication barrier before a visitor reaches the application backend. Strict zero-trust access blocks brute-force attacks and vulnerability exploits, but it breaks a common workflow: sharing a public link with users who do not have an SSO account. You can solve this by configuring Caddy to bypass the authentication gateway for specific URLs, exposing only the exact paths required for the share links to function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caddy Forward Auth Mechanics
&lt;/h2&gt;

&lt;p&gt;Caddy provides the &lt;code&gt;forward_auth&lt;/code&gt; directive to handle external authentication. It proxies a clone of the incoming HTTP request to an authentication gateway. If the gateway returns a successful 2xx status code, Caddy copies the authorized user headers and passes the request to your application. If the gateway returns any other status code, Caddy forwards the gateway's response back to the client. This usually triggers an HTTP 302 redirect to the SSO login portal.&lt;/p&gt;

&lt;p&gt;To protect an entire application, a basic Caddyfile block looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nextcloud.example.com {
    forward_auth authelia:9091 {
        uri /api/verify?rd=https://auth.example.com
        copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
    }
    reverse_proxy nextcloud:80
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration rejects all unauthenticated traffic. No public share links will load under this block because the authentication gateway intercepts every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Homelab Reverse Proxy Public Share Links
&lt;/h2&gt;

&lt;p&gt;To allow external users to access specific files without a login, you must split the Caddy configuration. Caddy uses named matchers and the &lt;code&gt;handle&lt;/code&gt; directive to apply rules selectively based on the request path.&lt;/p&gt;

&lt;p&gt;For Nextcloud, public shares are generated under the &lt;code&gt;/s/&lt;/code&gt; path. Bypassing this single path is not enough. The user will see a broken HTML page without styling. Nextcloud requires CSS, JavaScript, and image assets located in other directories to render the share page correctly.&lt;/p&gt;

&lt;p&gt;This Caddyfile snippet bypasses authentication for the required paths and protects the rest of the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nextcloud.example.com {
    # Match the share path and necessary asset directories
    @public path /s/* /index.php/s/* /apps/* /core/* /dist/* /img/* /js/* /css/* /avatar/* /svg/*

    # Allow public access to the matched paths
    handle @public {
        reverse_proxy nextcloud:80
    }

    # Protect everything else with Authelia
    handle {
        forward_auth authelia:9091 {
            uri /api/verify?rd=https://auth.example.com
            copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
        }
        reverse_proxy nextcloud:80
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup routes any request matching the &lt;code&gt;@public&lt;/code&gt; rules directly to Nextcloud. All other requests fall through to the second &lt;code&gt;handle&lt;/code&gt; block and hit the &lt;code&gt;forward_auth&lt;/code&gt; directive.&lt;/p&gt;

&lt;p&gt;You apply the same logic if you use Authentik instead of Authelia. The URI endpoint and the copied headers differ, but the Caddy &lt;code&gt;handle&lt;/code&gt; structure remains identical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    # Protect everything else with Authentik
    handle {
        forward_auth authentik:9000 {
            uri /outpost.goauthentik.io/auth/caddy
            copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid
        }
        reverse_proxy nextcloud:80
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The API Trap with Immich
&lt;/h2&gt;

&lt;p&gt;Not all applications handle public shares using simple static HTML paths. Modern platforms often operate as single-page applications that rely heavily on backend APIs.&lt;/p&gt;

&lt;p&gt;When you create a shared link in Immich, the frontend loads via the &lt;code&gt;/share/&lt;/code&gt; path. The frontend then makes asynchronous calls to the &lt;code&gt;/api/&lt;/code&gt; endpoints to fetch the actual photo data. If you bypass just the &lt;code&gt;/share/&lt;/code&gt; path, the user gets a blank interface. The initial page loads, but Authelia blocks the subsequent API requests.&lt;/p&gt;

&lt;p&gt;Whitelist the entire &lt;code&gt;/api/&lt;/code&gt; path and you expose the administrative endpoints, nullifying the security benefits of the reverse proxy. To securely expose Immich share links via Caddy, you must whitelist the exact API routes required for shared links. These specific routes change across major updates.&lt;/p&gt;

&lt;p&gt;Because maintaining a precise list of API paths requires constant intervention, administrators often deploy the community-maintained Immich Public Proxy. This runs as a separate, stateless Docker container that exclusively handles shared links. It strips metadata and blocks unauthorized paths automatically, removing the need to modify your Caddy forward auth rules for the main Immich instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caddy Protect Self Hosted Apps and Mobile Clients
&lt;/h2&gt;

&lt;p&gt;Many administrators implement Caddy protect self hosted apps setups to block automated scanners, but this approach breaks native mobile clients. Mobile applications for Nextcloud, Immich, and Jellyfin expect direct access to the application API. When Caddy intercepts a mobile app's API request and returns an Authelia HTML login redirect, the mobile app fails to parse the response. The application crashes or hangs. It will not display the SSO login portal.&lt;/p&gt;

&lt;p&gt;You have three options to handle native mobile applications:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bypass the API paths completely. This reduces the security benefit of the identity provider because scanners can hit the application's login form directly.&lt;/li&gt;
&lt;li&gt;Require a VPN like Tailscale or WireGuard on mobile devices. The application stays fully internal, and you only use the public domain for web access.&lt;/li&gt;
&lt;li&gt;Use application-specific bypass headers. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some mobile clients allow you to inject custom HTTP headers into your requests. You can configure Caddy to bypass &lt;code&gt;forward_auth&lt;/code&gt; if a specific secret header is present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;immich.example.com {
    @mobile_bypass header Immich-Skip-Authelia "SuperSecretString123"

    handle @mobile_bypass {
        reverse_proxy immich:2283
    }

    handle {
        forward_auth authelia:9091 {
            uri /api/verify?rd=https://auth.example.com
            copy_headers Remote-User
        }
        reverse_proxy immich:2283
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration acts as a static API key. If the header is present, the request bypasses Authelia. You must use TLS (HTTPS) on your public domain to encrypt the header in transit and prevent interception. &lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing Header Spoofing Vulnerabilities
&lt;/h2&gt;

&lt;p&gt;When Caddy validates a request through the authentication gateway, it copies identity headers to the backend application. The application reads these headers to log the user in automatically. &lt;/p&gt;

&lt;p&gt;You must ensure external users cannot spoof these headers to gain administrative access on the bypassed public routes. Caddy normally clears incoming headers before passing them to the application if configured properly, but explicit bypass blocks require manual intervention.&lt;/p&gt;

&lt;p&gt;In your Caddyfile, ensure the &lt;code&gt;reverse_proxy&lt;/code&gt; directive does not implicitly trust incoming identity headers from the outside. If an attacker visits a bypassed path like &lt;code&gt;/s/xyz&lt;/code&gt; and manually includes a &lt;code&gt;Remote-User: admin&lt;/code&gt; HTTP header, Caddy must strip it. The &lt;code&gt;forward_auth&lt;/code&gt; directive automatically overwrites these headers on a successful login, but for paths in the &lt;code&gt;@public&lt;/code&gt; block, no overwrite occurs.&lt;/p&gt;

&lt;p&gt;You must manually strip the headers from unauthenticated requests to prevent privilege escalation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;handle @public {
    request_header -Remote-User
    request_header -Remote-Groups
    request_header -Remote-Name
    request_header -Remote-Email
    reverse_proxy nextcloud:80
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This directive removes the headers before the request reaches Nextcloud. &lt;/p&gt;

&lt;p&gt;The backend application also needs verification. Nextcloud requires the &lt;code&gt;trusted_proxies&lt;/code&gt; array in its &lt;code&gt;config.php&lt;/code&gt; file to validate where the headers come from.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'trusted_proxies'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'192.168.1.100'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set this to the IP address of your Caddy server or Docker network gateway. If you leave &lt;code&gt;trusted_proxies&lt;/code&gt; empty or misconfigured, Nextcloud may accept spoofed headers from untrusted sources, rendering your SSO setup useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/homelab/ansible-homelab/" rel="noopener noreferrer"&gt;Ansible for Homelabbers: Automate From One Playbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/homelab/lets-encrypt-64-day-certificates-homelab-reverse-proxy-guide/" rel="noopener noreferrer"&gt;Let's Encrypt 64-Day Certificates: Homelab SSL Automation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/homelab/proxmox-homelab-setup/" rel="noopener noreferrer"&gt;Proxmox VE Homelab Setup: Install and Configure (2026)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does forward_auth break native mobile apps?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Mobile apps expect direct API access and fail when redirected to a login portal. You must either bypass the API paths, use custom headers, or rely on a VPN for mobile access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to bypass CSS and JS paths for share links?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usually, yes. Nextcloud share pages rely on assets in the /apps/ and /core/ directories. Bypassing only the /s/ path results in a broken layout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Authentik instead of Authelia?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Caddy's forward_auth directive works with any authentication gateway that supports the forward authentication specification.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://linuxcore.dev/homelab/caddy-forward-auth-public-share-links-homelab/" rel="noopener noreferrer"&gt;linuxcore.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>caddy</category>
      <category>authelia</category>
      <category>nextcloud</category>
    </item>
    <item>
      <title>Amazon ECS Gateway: Cheap Ingress for AWS Homelabs</title>
      <dc:creator>Sergej Voronko</dc:creator>
      <pubDate>Thu, 22 Oct 2026 04:46:56 +0000</pubDate>
      <link>https://dev.to/linuxcore/amazon-ecs-gateway-cheap-ingress-for-aws-homelabs-1714</link>
      <guid>https://dev.to/linuxcore/amazon-ecs-gateway-cheap-ingress-for-aws-homelabs-1714</guid>
      <description>&lt;p&gt;Amazon ECS Gateway solves the most expensive part of running hybrid homelab workloads on AWS: the idle Application Load Balancer (ALB). Instead of permanently running a dedicated ALB at $16.42 per month base fee (in us-east-1) at the time of writing just to wait for failover traffic, this managed ingress resource dynamically provisions your load balancer only when a container service connects. When your homelab is healthy and your cloud tasks scale to zero, ECS removes the ALB, completely eliminating the standby cost.&lt;/p&gt;

&lt;p&gt;Before October 2026, exposing an Amazon Elastic Container Service (ECS) cluster cheaply required painful trade-offs. You either ran AWS Fargate tasks with public IP addresses, which change on every deployment and force you to script your own Route 53 DNS updates, or you provisioned an ALB. An ALB provides static endpoints, AWS Certificate Manager (ACM) TLS certificates, and clean host-based routing, but AWS bills a base hourly rate regardless of traffic volume. For a homelab admin keeping a secondary Nextcloud or Bitwarden instance in AWS for disaster recovery, paying nearly $200 a year for an idle load balancer defeats the purpose of cheap cloud storage.&lt;/p&gt;

&lt;p&gt;Amazon ECS Gateway completely abstracts the load balancer away from individual services. It operates as an account-level infrastructure manager. You define your ingress standards just once, specifying your VPC subnets, security groups, and listener rules.&lt;/p&gt;

&lt;p&gt;When you create or update an ECS service, you opt in by adding a gateway connector parameter. ECS then automatically provisions the required resources directly in your account. If it is the first service connecting, ECS creates an ALB. It generates a unique DNS hostname, requests an ACM certificate, and configures the target groups automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing ALBs for Cheap ECS Ingress
&lt;/h2&gt;

&lt;p&gt;Running multiple lightweight containers in the cloud usually causes ALB sprawl. Standard configurations require either multiple ALBs or manual manipulation of listener rules to share one. ECS Gateway handles shared hosting natively. A single gateway supports up to 25 services sharing the same underlying ALB using host-based routing. You pay only for the actual infrastructure provisioned in your account. Whether you expose one homelab dashboard or twenty separate microservices, your base AWS bill remains tied to a single load balancer.&lt;/p&gt;

&lt;p&gt;Once you connect a 26th service, ECS automatically provisions a second ALB to handle the capacity, immediately doubling your base infrastructure cost. Keep your total active service count under the limit if budget optimization is your primary goal.&lt;/p&gt;

&lt;p&gt;Before ECS Gateway, sharing an ALB meant manually creating Route 53 DNS records for each new service and mapping them to the ALB canonical name. ECS Gateway handles the DNS provisioning automatically. Each service connecting to the gateway receives a unique DNS hostname instantly. You can point your homelab CNAME records directly at these dynamically generated endpoints. When the gateway tears down the ALB during scale-to-zero operations, your CNAME remains intact, ready to route traffic the moment the ALB spins back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale-to-Zero AWS Cloud Fallback
&lt;/h2&gt;

&lt;p&gt;The defining feature for hybrid homelabs is automated lifecycle management. The official documentation confirms that ECS removes the underlying ALB when no services remain connected to the gateway.&lt;/p&gt;

&lt;p&gt;If your local server loses power, you scale your standby ECS services from zero to one. ECS registers the connected service, provisions the ALB, requests the ACM certificate, and begins routing traffic. Your disaster recovery site comes online with secure ingress automatically configured. Once your local Proxmox host is restored and you scale the AWS services back to zero, ECS tears down the ALB. Your monthly AWS bill drops back to standard object storage and idle database rates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating an Amazon ECS Gateway
&lt;/h2&gt;

&lt;p&gt;Setting up an ECS Gateway requires an IAM role granting ECS permission to provision ALBs, security groups, and certificates on your behalf. Once the IAM role is ready, you create the gateway using the AWS CLI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecs create-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; homelab-fallback &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--infrastructure-role-arn&lt;/span&gt; arn:aws:iam::123456789012:role/ECSGatewayRole &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--configuration&lt;/span&gt; &lt;span class="s1"&gt;'{ "HostSharedAlb": { "networkConfiguration": { "subnets": [ "subnet-0abcd1234efgh5678" ] } } }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--configuration&lt;/code&gt; flag accepts a JSON object defining your infrastructure limits. By specifying &lt;code&gt;HostSharedAlb&lt;/code&gt;, you instruct the gateway to use standard Application Load Balancers configured for host-based routing. The &lt;code&gt;networkConfiguration&lt;/code&gt; block accepts your VPC subnets, dictating exactly where the load balancer interfaces are placed. If your ECS tasks sit in private subnets, ensure you select public subnets here so the ALB can route external internet traffic into your private tasks.&lt;/p&gt;

&lt;p&gt;Specific configuration settings are immutable. Your gateway name, traffic routing scheme, VPC, subnets, and listener port cannot be modified after creation. Modifying your subnets later requires creating an entirely new gateway and migrating your active services over.&lt;/p&gt;

&lt;h2&gt;
  
  
  ECS awsvpc Network Mode Requirements
&lt;/h2&gt;

&lt;p&gt;When defining your ECS task, you must specify the &lt;code&gt;awsvpc&lt;/code&gt; network mode. ECS Gateway relies on this specific networking mode to enforce security group rules. ECS automatically generates a service security group that permits inbound traffic exclusively from the ALB. This completely isolates your containers from direct internet access, even if placed in a public subnet.&lt;/p&gt;

&lt;p&gt;This requirement requires no changes if you use AWS Fargate, as &lt;code&gt;awsvpc&lt;/code&gt; is the default and only supported mode. If you run your containers on self-managed EC2 instances to save money, you must ensure your task definitions explicitly declare the &lt;code&gt;awsvpc&lt;/code&gt; mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS WAF and Security Integration
&lt;/h2&gt;

&lt;p&gt;Publicly exposing internal services carries risk, especially during an automated failover where you might not be actively monitoring logs. Previously, attaching an AWS WAF required manually linking it to an existing ALB. ECS Gateway accepts a WAF Web ACL directly in the core gateway configuration. &lt;/p&gt;

&lt;p&gt;When ECS provisions the underlying ALB, it automatically attaches the firewall rules. You can define IP allowlists limiting access strictly to your home static IP or your WireGuard exit node. If your homelab fails over to AWS, the WAF rules immediately protect the exposed containers from automated scraping and exploitation attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  ECS Gateway IAM Role Requirements
&lt;/h2&gt;

&lt;p&gt;Because ECS Gateway automates infrastructure creation, it depends on an infrastructure IAM role. ECS assumes this role to generate the ACM certificates, deploy the ALB, and configure the necessary security groups. You must create a dedicated role with trust policies allowing the &lt;code&gt;ecs.amazonaws.com&lt;/code&gt; service principal to assume it. &lt;/p&gt;

&lt;p&gt;Never reuse your standard ECS execution task roles for this purpose. The infrastructure role requires extensive permissions for Elastic Load Balancing, ACM, and Amazon EC2 security group management. Keeping this role strictly scoped prevents an attacker inside a compromised container from provisioning arbitrary AWS networking infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring ECS Gateway Costs
&lt;/h2&gt;

&lt;p&gt;While the load balancer provisioning happens automatically in the background, the resulting ALB exists directly in your AWS account. You retain full visibility into the network traffic. Standard ALB metrics automatically populate in Amazon CloudWatch. You can inspect target health, request latency, and HTTP 5xx error rates exactly as you would with a manually provisioned ALB. &lt;/p&gt;

&lt;p&gt;If a container crashes, standard Elastic Load Balancing target health checks mark the task unhealthy and stop routing traffic. Because the resources live visibly in your account, they also show up in AWS Cost Explorer, allowing you to track the exact hourly spend of your ALB while your disaster recovery site remains active.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/aws/when-to-move-homelab-to-aws/" rel="noopener noreferrer"&gt;When to Move Your Homelab Workload to AWS (And When Not To)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/aws/aws-cost-guide-spot-reserved-savings/" rel="noopener noreferrer"&gt;AWS Spot, Reserved, and Savings Plans: Cost Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/aws/ten-euro-month-aws-stack/" rel="noopener noreferrer"&gt;A Minimal AWS Stack: VPS, DNS, Monitoring, Backups&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linuxcore.dev/aws/aws-lambda-web-functions-homelab-api-guide/" rel="noopener noreferrer"&gt;Host Homelab APIs on AWS Lambda Web Functions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is Amazon ECS Gateway?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is an account-level resource that automates ingress for ECS services by dynamically provisioning Application Load Balancers, TLS certificates, and DNS records on demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Amazon ECS Gateway cost extra?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. The gateway itself is free. You only pay for the underlying infrastructure (like ALBs) while it is actively provisioned for connected services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use ECS Gateway for Fargate tasks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. ECS Gateway supports the awsvpc network mode, which is required for AWS Fargate.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://linuxcore.dev/aws/amazon-ecs-gateway-ingress-homelab-cost-guide/" rel="noopener noreferrer"&gt;linuxcore.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>aws</category>
      <category>ecs</category>
      <category>homelab</category>
    </item>
  </channel>
</rss>
