OK, this is a weird post, but after reading a post about running emacs with systemd, [1] I've realized that the my take on how I run and manage processes is a bit unique, and worth enumerating and describing. The short version is that I regularly run multiple instances of emacs, under systemd, in daemon mode, and it's pretty swell. Here's the rundown:

  • On Linux systems, I use a build of emacs with the lucid toolkit, rather than GTK, because of this bug with GTK, which I should write more about at some point. Basically, if the X session crashes with GTK emacs, even if you don't have windows open, the daemon will crash, even if the dameon isn't started from a GUI session. The lucid toolkit doesn't have this problem.

  • I run the process under my user's systemd instance, rather than under the PID 1 systemd instance. I like keeping things separate. Run the following command to ensure that your user's systemd will start at boot rather than at login:

    sudo loginctl enable-ligner $(whoami)
    
  • I have a systemd service file named emacs@.service in my ~/.config/systemd/user/ directory that looks like this:

    [Unit]
    Description=Emacs-tychoish: the extensible, self-documenting text editor.
    
    [Service]
    Type=forking
    ExecStart=/usr/bin/emacs --daemon=%i --chdir %h
    ExecStop=/usr/bin/emacsclient --server-file=hud --eval "(progn (setq kill-emacs-hook 'nil) (kill-emacs))"
    Restart=always
    TimeoutStartSec=0
    
    [Install]
    WantedBy=default.target
    

    I then start emacs dameons:

    systemctl --user start emacs@work
    systemctl --user start emacs@personal
    systemctl --user start emacs@chat
    

    To enable them so that they start following boot:

      systemctl --user enable emacs@work
      systemctl --user enable emacs@personal
      systemctl --user enable emacs@chat
    
    Though to be honest, I use different names for daemons.
    
  • I have some amount of daemon specific code, which might be useful:

    (setq server-use-tcp t)
    
    (if (equal (daemonp) nil)
        (setq tychoish-emacs-identifier "solo")
      (setq tychoish-emacs-identifier (daemonp)))
    
    ;; this makes erc configs work less nosily. There's probably no harm in
    ;; turning down the logging
    (if (equal (daemonp) "chat")
        (setq gnutls-log-level 0)
      (setq gnutls-log-level 1))
    
    (let ((csname (if (eq (daemonp) nil)
                 "generic"
                 (daemonp))))
      (setq recentf-save-file (concat user-emacs-directory system-name "-" csname "-recentf"))
      (setq session-save-file (concat user-emacs-directory system-name "-" csname "-session"))
      (setq bookmark-default-file (concat user-emacs-directory system-name "-" csname "-bookmarks"))
      (setq helm-c-adaptive-history-file (concat user-emacs-directory system-name "-" csname "--helm-c-adaptive-history"))
      (setq desktop-base-file-name (concat system-name "-" csname "-desktop-file"))
      (setq desktop-base-lock-name (concat system-name "-" csname "-desktop-lock")))
    

    Basically this just sets up some session-specific information to be saved to different files, to avoid colliding per-instance.

  • Additionally, I use the tychoish-emacs-identifier from above to provide some contextual information as to what emacs daemon/window I'm currently in:

    (setq frame-title-format '(:eval (concat tychoish-emacs-identifier ":" (buffer-name))))
    
    (spaceline-emacs-theme 'daemon 'word-count)
    (spaceline-define-segment daemon tychoish-emacs-identifier)
    

    Also, on the topic of configuration, I do have a switch statement that loads different mu4e configurations in different daemons.

  • To start emacs sessions, I use operations in the following forms:

    # create a new emacs frame. Filename optional.
    emacsclient --server-file=<name> --create-frame --no-wait <filename>
    
    # open a file in an existing (last-focus) frame/window. Filename required.
    emacsclient --server-file=<name> --no-wait <filename>
    
    # open a terminal emacs mode. Filename optional .
    emacsclient --server-file=<name> --tty --no-wait <filename>
    

    That's a lot to type, so I use aliases in my shell profile:

    alias e='emacsclient --server-file=personal --no-wait'
    alias ew='emacsclient --server-file=personal --create-frame --no-wait'
    alias et='emacsclient --server-file=personal --tty'
    

    I create a set of aliases for each daemon prefixing e/ew/et with the first letter of the daemon name.

And that's about it. When I've used OS X, I've managed something similar using launchd but the configuration files are a bit less elegant. On OS X, I tend to install emacs with cocoa toolkit, using homebrew.

Using multiple daemons is cool, though not required, for a number of reasons:

  • you can have good separation between personal things and professional/work things, which is always nice, but particularly gratifying during the pandemic when it's easy to work forever.
  • Managing multiple separation of email. While mu4e has profiles and contexts, and that's great, I like a firmer boundary, and being able maintain separate email databases.
  • Running emacs lisp applications that do a lot of networking, or do other blocking operations. The main case where this matters in my experience is running big erc instance, or something else that isn't easily broken into async/subprocesses.
[1]And commenting!