My development environment with my favourite tools and dotfiles packed as a Docker image.
This way I can use it as a base for per-project image and spin containers off of it, in order to separate my projects and keep my host system clean and state-less and therefore easily reinstallable.
Although containers are ephemeral, we can keep our SSH keys, shell history and other desirable files by using Docker volumes.
We can manage these images either manually, or with docker-project-manager, which I created specifically for this purpose.
- Development version of Emacs 28 with native compilation.
- NeoVim 0.4.3.
- Babashka 0.5.0.
- Clojure CLI 1.10.3.855.
- GH CLI 1.12.0 (2021-06-29).
- My dotfiles.
- APT sources for Node.js and Yarn.
Or babashka version (exec) or both.
I use iPad Pro as my main work machine and I work on my VPS, since Linux really is what I want as my development environment.
For that reason, my image is based on the OpenSSH server, and I can connect to my development environments directly, without having to touch the host machine at all.
However would you be using this locally, you’re better of not using SSH, but rather docker exec -it zsh
or whatever your favourite shell is.
Why? Because this way your ENV
, WORKDIR
and possibly other settings from your Dockerfile
will be respected.
By that I mean that if I put ENV AWS_DEV_SECRET=1234567890
into my Dockerfile
, if I connect with docker exec
, the variable AWS_DEV_SECRET
will be defined. However if I connect with the OpenSSH server, the variable is not going to be defined.
The rest of the documentation assumes your workflow is similar. Given how simple this is, I’m sure you can figure out for yourself which way is best for your usecase and adjust the build scripts appropriately.
First, let’s create a new directory for our project. Let’s call it rpm
:
mkdir rpm && cd rpm
Next, let’s start with the Dockerfile
. Obviously we use the dev
image as our base:
FROM jakubstastny/dev:latest
First, we need to copy our public SSH key into .ssh/authorized_keys
:
mkdir .ssh && chmod 700 .ssh && echo "<your-public-ssh-key>" > .ssh/authorized_keys
This is important, so you can SSH into the running container.
Next, let’s generate a new pair of SSH keys for the project:
mkdir .ssh && ssh-keygen -t rsa -C rpm -f .ssh/id_rsa
You will need to add the generated public SSH key into your GitHub settings, in order to be able to push to your repositories (and clone any private repository that you might need).
Now let’s copy the key pair to the image:
COPY .ssh /root/.ssh
This is really up to you. You might use something like this:
RUN ssh-keyscan -H github.com >> ~/.ssh/known_hosts && dotfiles pull -r && git clone [email protected]:jakub-stastny/dev.git
For your first image, you don’t have to worry about this. The default SSHD_PORT
is 2222
.
However if you want to run multiple of these images in paralel, you’ll need to override the SSHD_PORT
, so that it’s unique like so:
ENV SSHD_PORT=2223
First, let’s build the image:
docker build . -t rpm-dev-env
As you can see, the naming convention I use is <project-name>-dev-env
. It’s not necessary, but it’s a useful way of distinguishing the development environments from other Docker images.
Now let’s create the image:
docker create -it -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/.history:/root/.history --network host --name rpm-dev-env --hostname rpm rpm-dev-env
Let’s break down the most important parts:
Proxying /var/run/docker.sock
from the host to the development environment via -v /var/run/docker.sock:/var/run/docker.sock
is a way of doing Docker-in-Docker, also known as DinD.
It’s not the most secure way, probably using --privileged
flag would be better, but since I use my development environment as a stateless, ephemeral thing, I’m not really concerned with security.
Also note that I’ve been using this approach for many years: I’ve seen there are better ways of doing DinD these days, but I haven’t had the need to review them so far.
Unlike the SSH keys, which we simply COPY
to the image, shell history keeps changing and we don’t want to loose the changes when we rebuild the image.
That’s why we proxy it from the host machine as a volume using -v $PWD/.history:/root/.history
. If your shell history is not named .history
, replace the file name with the appropriate one.
Host networking means that we can forget about exposing ports manually: if you start a server on port 8000
in your development environment, it will be available on port 8000
on the host machine automatically. This is what --network host
is for.
docker start rpm-dev-env
Now you’re good to go. Assuming that you have the container on a VPS like I do, you can connect directly to it by SSH without having to go through the host machine first:
ssh root@ip:2222
As a side note, I highly recommend using Mosh instead of SSH. You won’t even notice you’re working on a remote machine, that’s how fast it is. And it always reconnects, even if you switch network.
- The development branch is
literate.dev
. - The stable branch is
literate.stable
. - Development documentation is generated from
literate.stable
. - Here is how to set up the development environment to hack on the image itself.
- Here is what I do to set up the host machine.
- Here is how I publish the documentation.
- And finally here is how I release the image.
Enjoy!