/doom-emacs-docker

Primary LanguageDockerfileMIT LicenseMIT

A Big Containerized Doom/Docker Emacs (Featuring Guix)

Introduction

This is a custom Doom Emacs configuration that I’ve dockerized using Guix, with Jaremko’s docker-x11-bridge web configuration as a default GUI. I’d consider this in alpha, for people somewhat familiar with (doom) emacs who might be curious about trying someone else’s setup.

Setup

Requirements

  • Docker
  • Access to port 10000
  • 12 GB of disk space: My bundled emacs is not resource intensive. But I’ve included space behemoths (like texlive, pandoc, and gcc tooling) which enable creating pdfs and accessing web pages as org-mode buffers.

Running in a terminal

You can execute the following in a shell:

git clone https://github.com/branjam4/doom-emacs-docker.git
cd doom-emacs-docker
docker-compose up -d --build

# accessing the emacs container
docker exec -it guixemacs_emacshome_1 /run/current-system/profile/bin/bash --login

#within the container
emacs

(Optional) Accessing in a web browser

Go to http://localhost:10000/index.html?encoding=rgb32&password=111 in a browser. You should see the Doom emacs splash screen!

The web browser works well for viewing images inside emacs buffers or previewing doom themes. But the screen can easily become blurry when we make changes. Additionally, it is not ideal for some keyboard shortcuts (more information on these in Issues: Xpra web).

I’m still learning about alternative GUI container options. But for now my quick workaround is switching from then back to the window (with Alt+Tab for example)

Fine-tuning

There are some quality-of-life settings to adjust; I describe how to change them in the headings below:

Find the config.org file

  • Execute the function doom/find-file-in-private-config to find the private config file, named config.org. More specifically:
    • Press Alt+x (emacs will recognize this as M-x), which brings up a menu where you can look up and execute any function with an (interactive) form in its definition body.
    • Search for the function (doom/find-file-in-private-config). Searching here is flexible (but is case sensitive), so you can try doom file priv and this function should still come up.
    • Once we have doom/find-file-in-private-config highlighted in the menu, press Return (known as RET to emacs)

Make configuration adjustments

Next, navigate around the file, specifically to the settings you want to change. I describe two optional changes:

Function discovery note
The most useful key for a beginner anyone looking to discover emacs functionality is M-x. Over time though, you might feel it’s more efficient to access commands via keyboard shortcuts, instead of through M-x. If the function you’re executing has a keyboard shortcut, you’ll see it immediately to the right of the function name as you’re M-x-ing for it. doom/find-file-in-private-config for example has the shortcut chord C-c f p (as in press Ctrl+c, then f, then p). There is also a menu that will pop up if you pause before finishing a chord, which can be helpful if you’ve forgotten the exact combination of the function you want to run.

Movement Keys

Emacs comes with a tutorial function, called help-with-tutorial. You can either click the link inside while reading this in emacs, or M-x for it. You may not be able to complete it within the web browser though. By default emacs assumes full control of the Ctrl or Alt/Meta modifier keys, but on a web browser this is not the case! You have two options for getting around this:

  1. (basic) Start another emacs instance inside the shell, instead of on the browser:
    docker exec -ti guixemacs_emacshome_1 /run/current-system/profile/bin/bash --login
    #within the container
    emacs -nw
        
  2. (advanced*) Use vi keybindings inside emacs**:
    • Search for ;;(evil +everywhere), and remove the semicolons. There are multiple ways to do the search, but I tend to prefer these functions:
      • Execute counsel-grep-or-swiper to filter through matches interactively as you type
      • Use Ctrl+s to bring up a quick search, bringing your cursor to the next match. Not as flashy as the first function, but it’s available even in a plain emacs.

*Advanced considering the steep learning curve of how to move around or make changes if you’re unaware of how vi’s modal editing works. In particular, going in (~i~) or out (~ESC~) of =insert mode= might confuse the unaware. That said, the creator/maintainer of Doom came from vim and designed Doom first for vimmers, so if you’re willing to learn (or already know) the basics, then going this route may provide a better out-of-the-box experience. **this may or may not go without saying, but the emacs tutorial becomes moot once you switch off emacs’ default keybindings. You’ll have to instead consult a vim tutorial.

Dictionary warning

It slipped my mind to install a dictionary for the emacs-flyspell package, so emacs will give a warning in your *Messages* buffer. It’s harmless, but you can remove it by searching for, then deleting the spell module. The module should be under the doom :checkers category.

Save and recompile Doom emacs

  • Execute the function org-babel-tangle (M-x, org-babel-tangle / org bab tan, confirm correct function, press RET). This propagates the changes made to the actual files Doom will read to reconfigure your environment. We call this tangling in the literate programming world. When you tell emacs to tangle your .org file, it will save before executing the tangle.

Reloads and Restarts

doom/reload-ing

Execute the function doom/reload whenever you change your configuration file. There are also some custom keybindings I define which only show up after a doom/reload. doom/reload will hot-reload emacs by:

  • installing/removing packages you’ve (un)declared in your config
  • running other changes you’ve chosen to make in the configuration (such as when to load a package or keybinding changes)
  • byte-compiling packages/composing an autoloads file/rerunning other optimization functions that help emacs run smoothly.

Exiting

Normally doom/reload does not require restarting emacs. But in the emacs -> vim keybinding change, functions which should now follow the SPC (space) key still follow the C-c key. So we will need to exit, by evaluating save-buffers-kill-terminal to quit emacs. Once you leave emacs, the container responsible for emacs will shut down; we will have to use docker from the command line to start it back up.

Restarting

Assuming we’re working with a stopped container, use docker start guixemacs_emacshome_1 in the command line. Then you can return to the web-interface.

Purpose

I’ve set up some custom workflows within emacs that I’d like others outside of emacs to interact with, either through self-serving exploration, or interactive demos. But I didn’t just want to share specific emacs appliances, I also wanted to empower others to use said appliances, and hopefully build their own. To me, this meant reducing the cognitive load of learning out-of-the-box emacs. It also meant eliminating the overhead of setting up a custom emacs config. More rambling in the On (re)producing a containerized emacs section.

Known Issues

Container size

Full-featured dev environments like Visual Studio recommend having 20-50GB of space available. Were I to attempt replication of their killer features (fine-tuned remote execution, LSP integration, etc.), perhaps my container would fall within that range (bringing with it greater responsibility to demonstrate what distinguishes this from Visual Studio).

While I still have increased my capability to share my work through dockerizing my development environment, 12GB might be a big ask on a local machine. So future work would look like:

  • providing a VPS that both I and someone else can ssh into.
  • creating an emacs cluster, where “child” emacs containers might be:
    • smaller, but longer running processes which would otherwise halt emacs
    • dedicated “apps” such as:
      • ERC
      • a mail reader
      • specialized org-mode use cases (issue tracker, website generation, jupyter backend)

The main idea here involves offloading core or peripheral functionality, such that users introduced to the “emacs cluster” don’t see a 12+GB hit, to their network or to their storage device (though of course it’s easier to remove inside Docker vs. outside of it).

Docker as a Virtual Machine

I tested this setup using the legacy Docker Toolbox (on Windows), in Virtualbox. In that case I had to set up port forwarding. Additionally, local volumes were not shared with the Docker virtual machine.

Guix functionality

Guix supports making per-user profiles. But with emacs in particular, there may be problems accessing emacs packages that live in a separate profile. I recommend beginners stick with the provided image, until they can learn a little more about how emacs knows about guix-installed packages.

Xpra web

If you followed along using the web interface, it’s very likely you dealt with a blurry screen whenever a lot of text shifted at once. Hard to say how xpra-through-http balances between performance and proper display as I don’t know. But it obviously presents a (second–given the keyboard issues–) potential barrier to the emacs-in-the-browser user experience. Setting up the web version of the docker-x11-bridge was more comfortable for me in the short term. But ssh is the way to go in the long run.

On (re)producing a containerized emacs

I’ve heard it said that a great part of emacs’ charm beyond its text editing capabilities is bringing disparate tools together under a unified text-first interface. But it’s the wild west in terms of what people have created so far which allows others to experience that charm for themselves (i.e. demo appliances). So I believe the “killer” emacs packages–magit, transient+hydra, ido/helm/ivy, tools built atop org-mode, even eshell–would greatly benefit from a collaborative infrastructure. It wasn’t emacs that got me into using these tools, rather these tools got me into emacs, along with the external tools attached to them (e.g. git, grep). Thus allowing more people to write menus, learn git, take control of organizing their lives, etc.–without having to first learn emacs, might have the side effect of getting more people to learn emacs.

Emacs-as-a-container is not new, but it doesn’t have a lot of traction compared to the default this emacs config is for me, you’re probably better off making your own culture. I don’t imagine that this effort will magically introduce an emacs built for social exploration and collaboration. But perhaps seeing this inspires you to want your own containerized setup to have friends try out, to use at a meeting where you have access to Docker but not your own dev machine, or to share the same environment while pairing.

If so, I have some avenues for you to consider:

Doom ready container

I saved a docker container with all the dependencies of Doom preinstalled, so you can roll your own doom config*, or let the installer give you the stock config *rolling your own config requires using guix within the container/your Dockerfile to install any external dependencies. You’ll need to take some specific steps to properly start and provision the container, which I document below:

docker pull j2a2m2all/docker-doomemacs-preinstall:latest
docker create --privileged <image_id> #allows you to use guix to install new packages, if necessary.
docker start <container_id>
docker exec -ti <container_id> /run/current-system/profile/bin/bash --login

#within the container
#starting as root
#become emacsuser then change to the home directory
su emacsuser
cd /home/emacsuser

#clone Doom emacs into .emacs.d
git clone https://github.com/hlissner/doom-emacs.git .emacs.d

#optional: clone your private config into .doom.d
#git clone https://github.com/branjam4/doom-config.git .doom.d

#optional: if you need external packages for your own config:
#guix install <packages>

#run doom's install script with the auto-confirm switch
.emacs.d/bin/doom -y install

It may be trivial to translate this into a Dockerfile, but I haven’t gotten around to pushing one yet.

Behind the layers: Guix System

This repository resembles a Frankenstein’s monster of abstractions: I take advantage of Docker for easy accessibility through DockerHub, but a lot of the “need-to-know” behind how it’s built comes from git repositories. In one of them (my Doom emacs config) I skip noting how to install some of the more cumbersome dependencies (libvterm, pandoc, texlive) since I do it through the other repo utilizing Guix (see this repo for the dependencies I use).

So what if you want to make your own container from scratch, replacing the extraneous dependencies I’ve put into it with your own?

#see the guix manual for follow-up
guix pack -f docker emacs <your> <dependencies> <here>

A substantial number of people working on Guix use emacs. That may not be true when looked at in the other direction though. So you may want to take the traditional route of installing your dependencies on top of a better known operating system. Initially that’s what I tried to do–install emacs on top of an Ubuntu image. But that quickly became a headache when it came to the sysadmin pieces of the puzzle:

  • where are the package repositories that come with the disk operating system?
  • how do I avoid Docker anti-patterns?
  • what are the steps to create an non-root user with a home directory and access to the environment?

I could only get so far in each of these avenues before wondering what other options I had. This is where Guix came in. Technically, Guix could take care of installing many, if not all the emacs package declarations I’ve made. But I wanted to leverage the work I’d already done on the Doom config, only using Guix where it falls short. Emacs does not aim to handle non-emacs dependencies and isn’t the greatest with (its own) containerization–but these are two things Guix excels at.