DigressEd.net

Occasional Digressions

Emacs + Elixir Config

Playing with Elixir + Phoenix and had to set up my emacs conf. There are many guides but many seem out of date and with too many complicated steps and options. I chose,

  • Native elixir-ts-mode (as of Emacs 30.1)
  • Configured with eglot instead of lsp.

My emacs setup is configured with Jack Rusher's emacs config cloned to ~/.emacs.d, and my own config (ep-local.el) from my repo of personal dot-files into ~/.emacs.d/lisp/. Jack has configured things nicely to let you extend his own config.

Instructions

On macOS, install elixir and elixir-ls (I also installed tree-sitter-cli but it might not be needed since we'll install the grammars from within emacs.

brew install elixir elixir-ls

In ep-local.el, I added:

;; elixir dev
(require 'eglot)
;; This is optional. It automatically runs `M-x eglot` for you whenever you are in `elixir-mode`
(add-hook 'elixir-mode-hook 'eglot-ensure)
(add-to-list 'eglot-server-programs `(elixir-mode "/opt/homebrew/bin/elixir-ls"))
;; If you access any projects via symlinks, and the lsp crashes immediately on startup in those projects, you might need this:
;;(setq find-file-visit-truename t)

;; built-in as of emacs 30.1
(require 'elixir-ts-mode)

;; https://github.com/ananthakumaran/exunit.el
(use-package exunit
  :config
  (add-hook 'elixir-mode-hook 'exunit)
  ;; Optionally configure `transient-default-level' to 5 to show extra switches
  ;; Or use `C-x l' to change the level of individual commands and switches
  ;; NOTE: default is 4
  (setq transient-default-level 5))

;; https://github.com/J3RN/inf-elixir
(use-package inf-elixir
  :bind (("C-c i i" . 'inf-elixir)
         ("C-c i p" . 'inf-elixir-project)
         ("C-c i l" . 'inf-elixir-send-line)
         ("C-c i r" . 'inf-elixir-send-region)
         ("C-c i b" . 'inf-elixir-send-buffer)
         ("C-c i R" . 'inf-elixir-reload-module)
         ("C-c C-z" . 'previous-multiframe-window)))


;; amend your auto-mode-alist:
(setq auto-mode-alist
      (append
       '(
         ;; ...

         ("\\.ex\\'" . elixir-ts-mode)
         ("\\.heex\\'" . elixir-ts-mode)
         ;; ...
         )
       auto-mode-alist))

With the configuration in place, launch Emacs and run

M-x treesit-install-language-grammar

You'll need to do it twice,

  1. for elixir using https://github.com/elixir-lang/tree-sitter-elixir as the source repo,
  2. for heex (for Phoenix) using https://github.com/phoenixframework/tree-sitter-heex.

I used default for all other prompts, but this probably assumes you have a dev setup already configured.

Published: 2025-09-30

Tagged: elixir emacs

Stubble & Co 20L Hybrid backpack

I've owned a Brenthaven 15/17 backpack for -- what feels like -- close to 20 years. I had to search for it and found a French review for it dated Dec 2008. This thing is a tank and has served me well including hundreds of work and personal trips. It met all my needs for 2008 me: lots of pockets, large laptop pocket, mesh pockets, zippered pockets, comfortable to wear.

2025-me, however, has been needing something more practical for current needs. For one, laptops are no longer thick as they were back then, plus as bulky as it was and, with as many pocket as it has, it is just not super practical for a 2-day work trip or even a morning starting at the gym. So a couple of months ago I started researching bags and immediately got overwhelmed by all the choices and abandoned several opened tabs in a browser window somewhere. Until last week, when I decided to bite-the-bullet.

Stubble and Co's lineup drew my attention, and I was initially looking at the Hybrid 30L, which seemed great for exactly those things I needed but also seemed a bit big - one of the things I got tired of about the Brenthaven one. So I started looking at the Everyday 20L which looks so nice with all the organizing and seemed about the right size for my height (176cm) and was set to buy it when I noticed a 20L version of the Hybrid got introduced. The problem is there are no reviews for it, since it's so new. And also, the photos on the site don't show as much detail of what the inside looks like compared to the photos on the page for the 30L.

After discussing with my partner (a many-bagger), she suggested I go with the clamshell opening for convenience. I spent two more nights looking at photos, even considered ordering both to compare and returning one, but finally decided to go with the hybrid 20L (Sand color option) along with the Wash bag to replace my not-very-useful toiletry packs.

The 20L Hybrid, in Sand

So far, I'm very happy with it. Will it last 20 years as well? Maybe not. But many people seem to be traveling with the 30L and say the quality is great. Also I probably won't be hauling mine everywhere like I did with the Brenthaven as I don't travel for work like I used to.

The main downsides are:

  1. Not as many pockets as the 30L, which has two big mesh pockets on the clamshell lid, one which is accessible from the outside.
  2. No mesh pockets period. There's only one zippered pocket inside and one unzippered in the laptop side.
  3. No key clips. This was also the case on the 30L. My brain seems to think I'm really going to miss that from the Brenthaven (and basically, every other backpack I've used).
  4. No compression strap, like on the 30L. Not sure if I'll miss that yet.

That said, so far, it feels like I made the right choice to go with the 20L. And maybe I just had too many pockets before because I did find a bunch of crap in my old bag that I didn't throw in the new one. The outside zip pocket works for flat quick-access items (work badge, phone charging cable, masks).

The 20L Hybrid, in Sand

I can fit the laptop charger and cable, and a small stand for my phone in the zippered pocket in the main compartment:

The 20L Hybrid, in Sand

This photo is to show how big the wash bag is, compared to my small Tech pouch and over-the-ear headphones.

The 20L Hybrid, in Sand

I tested packing as I would for a 2-day trip:

The 20L Hybrid, in Sand

And also for a morning session at the gym with shoes, change of clothes, workout hand-towel:

The 20L Hybrid, in Sand

And a shower towel on top:

The 20L Hybrid, in Sand

Loosening the compression straps to close it up:

The 20L Hybrid, in Sand

The Brenthaven didn't have a bottle sleeve but this one does, which fits my 500ml bottle nicely:

The 20L Hybrid, in Sand

And, of course, laptop (14") in there too:

The 20L Hybrid, in Sand

This is already far better than before as I would carry the Brenthaven and a duffel-bag with all my gym gear in there.

The pocket in the laptop compartment is small:

The 20L Hybrid, in Sand

The Brenthaven had a rigid sleeve pocket that kept papers nice and flat. I'll have to get a folder to throw in there as an alternative.

Finally, for any trips where I'll need to bring a carry-on, this is also much more convenient:

The 20L Hybrid, in Sand

I've already taken it on a bike to the hardware store to pick up some bulbs and other things for the house.

Published: 2025-09-07

Tagged: travel gear backpacks

NixOS/nginx/acme with mutliple vhosts

(TL;DR: skip to the config)

I recently migrated my VPS from arch to nixos which also meant migrating my apache2 conf to nginx since it's what seems to be most commonly used. I got 90% of the way with my various domains (all of static content) but the last bit took a lot of patience as I couldn't find examples that handled all my cases:

  • domain 1 (this blog):
    • http redirected to https.
    • www. redirected to root domain, hosting the about page.
    • pages served from a separate location for the blog. subdomain.
    • a redirect for another subdomain to an external site.
  • domain 2, another personal site, nothing unexpected:
    • http redirected to https.
    • www. redirected to root domain.
  • domain 3, a domain not hosted on the VPS, just a redirect to an external site.
    • needs http and https redirected to the external site.

I thought tweaking the conf a bit would be trivial but, while trying suggestions from other posts and even NixOS's Wiki all I ended up doing was :

  • arbitrary subdomains redirected to another vhost,
  • broken https redirects,
  • getting rate-limited by Let's Encrypt because I didn't realize the implications of changing the acme config (newbie error).

I got all frustrated and commented out the entire acme / nginx config and focusing on other things while the Let's Encrypt restriction passed.

I determined the main source of my problems were the catch-all vhosts suggested on the Wiki and other places plus some missing forceSSL = true which turns out are really needed. They catch-all hosts seem ideal but it is not straightforward to follow as soon as you have different sub-domains that should not all be redirected to the same base host. I finally started re-enabling one by one, using staging certificates, and got it all working. Surely it could be simplified but that's for another time.

Sample Config

# set up files / permissions
systemd = {
  # for nginx to write logs
  services.nginx.serviceConfig.ReadWritePaths = [ "/var/log/nginx" ];

  # where all files for the sites are in the filesystem - referenced
  # later in the nginx config
  tmpfiles.settings = {
    www = {
      "/var/www/domain-1.net" = {
        f = { user = "<your user id>"; group = "nginx"; };
      };
      "/var/www/domain-2.com" = {
        f = { user = "<your user id>; group = "nginx"; };
      };
    };
  };
};

# let's encrypt certificates
security = {
  acme = {
    acceptTerms = true;
    defaults = {
      email = "<your contact email>";
      group = "nginx";

      # Be aware there are rate-limits so too many changes to your
      # certs config may result in rejected requests for 24-hours.
      # Instead, uncomment the server option to enable use of the
      # staging environment
      #server = "https://acme-staging-v02.api.letsencrypt.org/directory";
      #
      # If you need to force-regenerate certificates, you can do so
      # with:
      #   sudo systemctl clean --what=state acme-domain-1.net.service
      #
    };

    certs = {
      "domain-1.net" = {
        webroot = "/var/lib/acme/acme-challenge";
        # list all subdomains that must share the certificate
        extraDomainNames = [ "www.domain-1.net" "sub.domain-1.net" "redir.domain-1.net" ];
      };
      "domain-2.com" = {
        webroot = "/var/lib/acme/acme-challenge";
        extraDomainNames = [ "www.domain-2.com" ];
      };
      "domain-3.com" = {
        webroot = "/var/lib/acme/acme-challenge";
        extraDomainNames = [ "www.domain-3.com" ];
      };
    };
  };
};

# finally the web server config
services.nginx = {
  enable = true;
  logError = "stderr emerg";
  package = pkgs.nginxStable.override { openssl = pkgs.libressl; };

  virtualHosts = {

    domain-1 = {
      serverName = "domain-1.net";         # matches domain used in security.acme.certs
      forceSSL = true;                     # needed to handle http://domain-1.net -> https://domain-1.net redirection
      enableACME = true;
      root = "/var/www/domain-1.net/html"; # where the site's files are found

      # acme path for request validation. `root` must match
      # `webroot` in security.acme.certs.<domain>
      locations."/.well-known/acme-challenge" = {
        root = "/var/lib/acme/acme-challenge";
      };

      extraConfig = ''
         charset utf-8;
         # error_log /var/log/nginx/domain-1_error_log emerg; # uncomment if you want separate error log file
         access_log /var/log/nginx/domain-1_access_log;
      '';
    };

    # www version of domain-1.net. Needs the acme location for cert
    # validation. All other requests are dedirected to https://domain-1.net/<path>
    "www.domain-1.net" = {
      serverName = "www.domain-1.net";
      forceSSL = true;
      useACMEHost = "domain-1.net";

      locations = {
        "/.well-known/acme-challenge" = {
          root = "/var/lib/acme/acme-challenge";
        };

        "/" = {
          return = "301 https://domain-1.net$request_uri";
        };
      };
    };

    # separate subdomain served from a different location
    blog = {
      serverName = "blog.domain-1.net";
      forceSSL = true;
      useACMEHost = "domain-1.net";
      root = "/var/www/domain-1.net/blog";

      locations."/.well-known/acme-challenge" = {
        root = "/var/lib/acme/acme-challenge";
      };

      # optional, if you want separate logs
      extraConfig = ''
         charset utf-8;
         # error_log /var/log/nginx/blog_domain-1_error_log emerg; # uncomment if you want separate error log file
         access_log /var/log/nginx/blog_domain-1_access_log;
      '';
    };

    # separate subdomain, redirected to external host
    sub = {
      serverName = "sub.domain-1.net";
      forceSSL = true;
      useACMEHost = "domain-1.net";

      locations = {
        "/.well-known/acme-challenge" = {
          root = "/var/lib/acme/acme-challenge";
        };
        "/" = {
          return = "301 https://externalhost.ext;
        };
      };
    };

    # second vhost, with only www. and non-www. versions. www. is
    # redirected to non-www. Defines aliases for resources outside
    #  of root path
    domain-2 = {
      serverName = "domain-2.com";
      forceSSL = true;
      enableACME = true;
      root = "/var/www/domain-2.com/html"; # / is served from .../html/ path

      locations = {
        "/.well-known/acme-challenge" = {
          root = "/var/lib/acme/acme-challenge";
        };
        # point to a resources/ not within /html/
        "/css/" = {
          alias = "/var/www/domain-2.com/resources/css/";
        };
        "/js/" = {
          alias = "/var/www/domain-2.com/resources/js/";
        };
        # redirect domain-2.com/info to domain-2.com/about
        "/info" = {
          return = "301 https://domain-2.com/about/";
        };
      };

      extraConfig = ''
         charset utf-8;
         # error_log /var/log/nginx/domain-2_error_log emerg; # uncomment if you want separate error log file
         access_log /var/log/nginx/domain-2_access_log;
      '';
    };

    # www version of domain-2.com. Needs the acme location for cert
    # validation. All other requests are dedirected to https://domain-2.com/<path>
    "www.domain-2.com" = {
      serverName = "www.domain-2.com";
      forceSSL = true;
      useACMEHost = "domain-2.com";

      locations = {
        "/.well-known/acme-challenge" = {
          root = "/var/lib/acme/acme-challenge";
        };

        "/" = {
          return = "301 https://domain-2.com$request_uri";
        };
      };
    };

    # a third domain - No files are hosted.
    # only here to handle ssl certificate and redirect both
    # domain-3.com and www.domain-3.com to an external site (e.g., soundcloud)
    "domain-3.com" = {
      serverName = "domain-3.com";
      serverAliases = [ "www.domain-3.com" ];
      forceSSL = true;
      enableACME = true;

      locations."/.well-known/acme-challenge" = {
        root = "/var/lib/acme/acme-challenge";
      };

      locations."/" = {
         return = "301 https://soundcloud.com/myband";
      };
    };
  };
};

Published: 2024-09-07

Tagged: nixos vps vhosts acme nginx

Archive
©1971-2025, Ed Porras