<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Tools on hxmn.dev</title><link>https://hxmn.dev/tools/</link><description>Recent content in Tools on hxmn.dev</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 16 Apr 2026 10:00:00 +0300</lastBuildDate><atom:link href="https://hxmn.dev/tools/index.xml" rel="self" type="application/rss+xml"/><item><title>My Networking Setup</title><link>https://hxmn.dev/tools/my-networking-setup/</link><pubDate>Thu, 16 Apr 2026 10:00:00 +0300</pubDate><guid>https://hxmn.dev/tools/my-networking-setup/</guid><description>&lt;p&gt;I run machines in a few different places — a home lab, a cloud VPS, a laptop that travels with me. Keeping them all on one coherent private network, with stable names and encrypted transport, is the thing I did not want to think about every day. This page collects the pieces I landed on.&lt;/p&gt;
&lt;h2 id="service-connectivity"&gt;&lt;a href="#service-connectivity" class="header-anchor"&gt;&lt;/a&gt;Service Connectivity
&lt;/h2&gt;&lt;p&gt;For the private network I use &lt;a class="link" href="https://headscale.net/" target="_blank" rel="noopener"
 &gt;headscale&lt;/a&gt; — an open-source, self-hosted implementation of the Tailscale control server. Clients are the unmodified Tailscale clients; only the coordination server is mine.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 HS[Headscale&lt;br/&gt;control server]

 subgraph tailnet [Tailnet]
 L[Laptop]
 V[Cloud VPS]
 R[Subnet router&lt;br/&gt;home lab]
 end

 DB[(Postgres&lt;br/&gt;10.0.0.5)]
 D[DERP relay]

 L -. keys / ACLs .-&gt; HS
 V -. keys / ACLs .-&gt; HS
 R -. keys / ACLs .-&gt; HS

 L &lt;== WireGuard ==&gt; V
 L &lt;== WireGuard ==&gt; R
 R --&gt; DB

 L &lt;-. fallback .-&gt; D
 V &lt;-. fallback .-&gt; D&lt;/pre&gt;&lt;p&gt;The control plane (dotted lines) only hands out keys and policy; all service traffic (thick lines) is direct WireGuard between peers. DERP is a relay used only when a direct path cannot be established.&lt;/p&gt;
&lt;h3 id="why-headscale"&gt;&lt;a href="#why-headscale" class="header-anchor"&gt;&lt;/a&gt;Why headscale
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sovereignty.&lt;/strong&gt; Account, keys, and ACLs live on a box I control. No SaaS account to expire, no per-device limits, no upstream outage breaking my mesh re-keying.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero-config client side.&lt;/strong&gt; Every node — macOS, Linux, iOS, router — runs the same Tailscale client I would use anyway. No custom builds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WireGuard underneath.&lt;/strong&gt; Fast, modern, encrypted by default. I do not run a classic VPN gateway.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="typical-deployment"&gt;&lt;a href="#typical-deployment" class="header-anchor"&gt;&lt;/a&gt;Typical deployment
&lt;/h3&gt;&lt;p&gt;The server is a small VPS with a public IP. A single Go binary (&lt;code&gt;headscale serve&lt;/code&gt;) listens on &lt;code&gt;localhost:8080&lt;/code&gt; and a reverse proxy (Caddy, in my case) terminates TLS for &lt;code&gt;https://headscale.example.com&lt;/code&gt;. Configuration lives in &lt;code&gt;/etc/headscale/config.yaml&lt;/code&gt; — the defaults are close enough that I only touched a handful of fields:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;server_url&lt;/code&gt; — the public URL.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;listen_addr&lt;/code&gt; — loopback, since Caddy fronts it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip_prefixes&lt;/code&gt; — the CGNAT range for the tailnet (the default &lt;code&gt;100.64.0.0/10&lt;/code&gt; is fine).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dns.magic_dns: true&lt;/code&gt; — so every node gets a stable DNS name inside the tailnet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;State is a single SQLite file; backing it up is &lt;code&gt;cp&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="onboarding-a-node"&gt;&lt;a href="#onboarding-a-node" class="header-anchor"&gt;&lt;/a&gt;Onboarding a node
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;Create a user on the control server: &lt;code&gt;headscale users create ilya&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Issue a short-lived pre-auth key: &lt;code&gt;headscale preauthkeys create -u ilya --expiration 1h&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On the new node: &lt;code&gt;tailscale up --login-server https://headscale.example.com --authkey &amp;lt;key&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That is the whole provisioning flow. The node joins the mesh, picks up its tailnet address, and is reachable by MagicDNS name from every other node.&lt;/p&gt;
&lt;h3 id="reaching-services"&gt;&lt;a href="#reaching-services" class="header-anchor"&gt;&lt;/a&gt;Reaching services
&lt;/h3&gt;&lt;p&gt;With the mesh in place, service connectivity becomes boring in the best way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stable names.&lt;/strong&gt; A Postgres instance on a home-lab host is just &lt;code&gt;homelab-db:5432&lt;/code&gt; from anywhere on the tailnet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ACLs.&lt;/strong&gt; Headscale supports Tailscale-style policy files. I scope which tags can reach which ports — the laptop can reach &lt;code&gt;tag:dev&lt;/code&gt;, CI runners can reach &lt;code&gt;tag:infra&lt;/code&gt;, nothing else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subnet routers.&lt;/strong&gt; A single node advertises &lt;code&gt;10.0.0.0/24&lt;/code&gt; from the home lab so I can reach devices that do not run Tailscale (a NAS, a switch&amp;rsquo;s management interface).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No inbound ports.&lt;/strong&gt; Services never need to be reachable on the public internet. WireGuard punches out; the control server only coordinates.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="derp"&gt;&lt;a href="#derp" class="header-anchor"&gt;&lt;/a&gt;DERP
&lt;/h3&gt;&lt;p&gt;Two nodes that cannot reach each other directly (both behind strict NATs) fall back to a DERP relay. I use Tailscale&amp;rsquo;s public DERP pool for this — headscale lets me mix in my own later if I ever need guaranteed-path control, but I have not needed to.&lt;/p&gt;</description></item><item><title>My Neovim Setup</title><link>https://hxmn.dev/tools/neovim-setup/</link><pubDate>Mon, 06 Apr 2026 11:00:00 +0300</pubDate><guid>https://hxmn.dev/tools/neovim-setup/</guid><description>&lt;p&gt;I have been using Neovim as my primary editor for several years. The setup has gone through many iterations and landed on something minimal and fast.&lt;/p&gt;
&lt;h2 id="philosophy"&gt;&lt;a href="#philosophy" class="header-anchor"&gt;&lt;/a&gt;Philosophy
&lt;/h2&gt;&lt;p&gt;Less is more. Every plugin must earn its place. If I can do it with a keymap and a built-in command, I skip the plugin.&lt;/p&gt;
&lt;h2 id="core-plugins"&gt;&lt;a href="#core-plugins" class="header-anchor"&gt;&lt;/a&gt;Core Plugins
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;lazy.nvim&lt;/strong&gt; — plugin manager. Lazy-loads everything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nvim-lspconfig&lt;/strong&gt; — LSP configuration for gopls, typescript-language-server, lua_ls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nvim-cmp&lt;/strong&gt; — completion engine. Sources: LSP, buffer, path.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;treesitter&lt;/strong&gt; — syntax highlighting, text objects, incremental selection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;telescope.nvim&lt;/strong&gt; — fuzzy finder for files, grep, LSP symbols.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;oil.nvim&lt;/strong&gt; — file explorer as a buffer. Edit the filesystem like text.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="lsp-configuration"&gt;&lt;a href="#lsp-configuration" class="header-anchor"&gt;&lt;/a&gt;LSP Configuration
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;lspconfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;lspconfig&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;lspconfig.gopls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;gopls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;analyses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;unusedparams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;staticcheck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;lspconfig.ts_ls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="key-mappings"&gt;&lt;a href="#key-mappings" class="header-anchor"&gt;&lt;/a&gt;Key Mappings
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;vim.keymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;gd&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vim.lsp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf.definition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;vim.keymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;gr&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vim.lsp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf.references&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;vim.keymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;K&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vim.lsp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf.hover&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;vim.keymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;lt;leader&amp;gt;rn&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vim.lsp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf.rename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;vim.keymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;lt;leader&amp;gt;ca&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vim.lsp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf.code_action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="what-i-skipped"&gt;&lt;a href="#what-i-skipped" class="header-anchor"&gt;&lt;/a&gt;What I Skipped
&lt;/h2&gt;&lt;p&gt;No file tree (oil.nvim is better). No status line plugin (the built-in one is fine). No git gutter (I use the CLI). No AI completion (I prefer explicit requests).&lt;/p&gt;</description></item><item><title>Taskfile: A Modern Alternative to Make</title><link>https://hxmn.dev/tools/taskfile/</link><pubDate>Sat, 04 Apr 2026 14:00:00 +0300</pubDate><guid>https://hxmn.dev/tools/taskfile/</guid><description>&lt;p&gt;Makefiles work. They have worked for decades. But they have sharp edges — tab sensitivity, implicit rules, &lt;code&gt;.PHONY&lt;/code&gt; everywhere, and arcane syntax for anything beyond the basics.&lt;/p&gt;
&lt;p&gt;Taskfile is a YAML-based task runner that does what most people actually use Make for, without the footguns.&lt;/p&gt;
&lt;h2 id="installation"&gt;&lt;a href="#installation" class="header-anchor"&gt;&lt;/a&gt;Installation
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install go-task
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="basic-taskfile"&gt;&lt;a href="#basic-taskfile" class="header-anchor"&gt;&lt;/a&gt;Basic Taskfile
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;3&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Build the application&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cmds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;go build -o bin/app ./cmd/app&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Run all tests&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cmds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;go test ./...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Run linters&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cmds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;golangci-lint run&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Run with live reload&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;build]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cmds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;air&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="what-i-like"&gt;&lt;a href="#what-i-like" class="header-anchor"&gt;&lt;/a&gt;What I Like
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Variables and environment files:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.env&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cmds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;flyctl deploy --app {{.APP_NAME}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Task dependencies with parallelism:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;lint, test, build]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dependencies run in parallel by default. No need to wire up &lt;code&gt;&amp;amp;&lt;/code&gt; and &lt;code&gt;wait&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conditional execution:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s1"&gt;&amp;#39;proto/**/*.proto&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;generates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s1"&gt;&amp;#39;gen/**/*.go&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cmds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;buf generate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The task only runs if source files are newer than generated files.&lt;/p&gt;
&lt;h2 id="when-to-keep-make"&gt;&lt;a href="#when-to-keep-make" class="header-anchor"&gt;&lt;/a&gt;When to Keep Make
&lt;/h2&gt;&lt;p&gt;If your project already has a Makefile and the team is comfortable with it, switching gains you nothing. Taskfile shines on new projects and for teams that are tired of debugging tab-vs-space issues.&lt;/p&gt;</description></item><item><title>Nix for Reproducible Dev Environments</title><link>https://hxmn.dev/tools/nix-for-dev-environments/</link><pubDate>Thu, 02 Apr 2026 10:00:00 +0300</pubDate><guid>https://hxmn.dev/tools/nix-for-dev-environments/</guid><description>&lt;p&gt;Every project has a list of dependencies: Go 1.22, Node 20, PostgreSQL 16, protoc, golangci-lint. The traditional approach is a wiki page that says &amp;ldquo;install these things.&amp;rdquo; It is always out of date.&lt;/p&gt;
&lt;p&gt;Nix solves this by declaring dependencies in a file that deterministically provides the exact versions you need, isolated from the rest of your system.&lt;/p&gt;
&lt;h2 id="getting-started-with-devenv"&gt;&lt;a href="#getting-started-with-devenv" class="header-anchor"&gt;&lt;/a&gt;Getting Started with devenv
&lt;/h2&gt;&lt;p&gt;devenv is a friendly wrapper around Nix for development environments. Install it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-env -iA devenv -f https://github.com/NixOS/nixpkgs/tarball/nixpkgs-unstable
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create a &lt;code&gt;devenv.nix&lt;/code&gt; in your project:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;languages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;languages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;go_1_22&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;golangci-lint&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;postgresql_16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;initialDatabases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;myapp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Enter the environment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;devenv shell
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You now have the exact Go version, tools, and a running PostgreSQL instance. No global installs. No version conflicts.&lt;/p&gt;
&lt;h2 id="direnv-integration"&gt;&lt;a href="#direnv-integration" class="header-anchor"&gt;&lt;/a&gt;direnv Integration
&lt;/h2&gt;&lt;p&gt;Add a &lt;code&gt;.envrc&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;use devenv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now every time you &lt;code&gt;cd&lt;/code&gt; into the project, your shell automatically loads the right tools. Leave the directory, and they disappear.&lt;/p&gt;
&lt;h2 id="why-this-matters"&gt;&lt;a href="#why-this-matters" class="header-anchor"&gt;&lt;/a&gt;Why This Matters
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;New team members run &lt;code&gt;devenv shell&lt;/code&gt; and they are ready. No setup docs.&lt;/li&gt;
&lt;li&gt;CI uses the same Nix definitions, so local and CI environments are identical.&lt;/li&gt;
&lt;li&gt;Upgrading a tool is a one-line change in &lt;code&gt;devenv.nix&lt;/code&gt;, reviewed in a PR like any other code change.&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>