Cover image of a fisherman's boat with a nest full of fishes.
This article assumes that you are familiar with the GNU/Linux tools and operating systems that are based on this project. It also assumes you already have Docker installed on your machine and a slight understanding of how things work with Docker. Some experience in using the Fish shell is also appreciated, although not mandatory to follow along with this article.
If you have worked with Docker, you know that this is a fantastic tool to create isolated environments to work with. The downside of it is that it requires some setup before being ready to use.
When it comes to testing, it is good to have our programs installed locally (for instance Node) to test things out and then spin up a Docker container when it comes to writing things down.
But wouldn't be that great to fully leverage Docker from the testing/toying part to the production phase?
This article aims at reducing this friction by using Docker and Fish Functions!
If you were to test a Node package for instance (you'll see later that this article applies to Python, Ruby, and virtually any programming language and even program), you will surely use NPX to try things out, for instance, a TypeScript program that displays Hello world!
$ touch index.ts
const message: string = "Hello world!"; console.log(message);
Simple enough. Let's try to see how it can be done using NPX.
npx ts-node index.ts Hello world!
Yay, it's working! NPX is great when it comes to testing the package because it does actually install it in a temporary space in your operating system.
Let's see now how we could use Docker (if we didn't have NPX installed on our computer).
$ docker run --volume "$PWD:/usr/local/src" --workdir /usr/local/src --user (id -u):(id -u) node npx ts-node index.ts Hello world!
Gosh... That's a lot of character for a simple program! I will definitely not use this when it comes to testing things quickly.
But at the same time, it requires me to have NPX installed as well as Node, and to keep them updated frequently if I want to use them on packages that need some bleeding-edge functionalities.
What if we could write some sort of function that can reuse these lines of code in a short alias.
You guessed it, Fish Functions are the answer to that. When you have a long set of lines that needs to be used to run some program, you can write a function that is, just like in TypeScript, used to call a set of instructions that you are often using in your daily workflow.
Let's create our folder that will contain our functions first if this is not already the case.
$ mkdir $HOME/.config/fish/functions
Now let's create our NPX function.
$ touch $HOME/.config/fish/functions/npx.fish
We can now edit this file and add the following to it.
function npx docker run --volume "$PWD:/usr/local/src" --workdir /usr/local/src node npx ts-node index.ts end
And to use it, simply close and re-open your terminal and type the following.
$ npx Hello world!
Looking good! This works great because Fish Functions are autoloaded when the shell is started, meaning that all of your functions are exposed as programs that can be used everywhere. So good!
But what if we are not using TypeScript and we want to run something else? We have to make some slight modifications to our function to make this dynamic.
function npx docker run --volume "$PWD:/usr/local/src" --workdir /usr/local/src node npx $argv end
The part that is changing here is the added
$argv. This is a special variable in Fish that contains the argument that is passed to the program that is called when calling a function as a program. This means that we can now pass any arbitrary options to NPX as we please.
$ npx ts-node index.ts Hello world! $ npx create-react-app my-awesome-app
Awesome! Now we can fully leverage the power of Docker to spin up our favorite programs without the heavy lifting of the Docker CLI options!
This means that you can use any program that has an image on the Docker Hub as a program. If you want to try out Ruby for instance, without the duty of keeping it up to date, you can create a simple Fish Function that will use Docker to run the Ruby image!
If you successfully reached this part of the article, this means that you can now:
- Create functions for customizing your Fish shell
- Use those functions to create analogous programs that work almost the same as before, but leveraging Docker
- Can adapt this example to other programs such as Ruby for instance
Of course, this is not at all a call for replacing all of your programs with Docker & Fish Functions. There will always be cases where you would need to install your programs locally and run them from here (like Git for instance). The choice to make a program run with Docker or run locally is yours to make and I trust you to do your part in concluding about whether to make a program isolated or not.
Although what I showcased was an example using the Fish shell, if you are comfortable enough with your own preferred shell like Bash or Zsh this article should serve you as a base that can be used to create your own functions in your own shell since the idea remains the same: create a reusable piece of code (function) to leverage the Docker engine and run image of your preferred programs.
Show me examples in the comment section that use other shells than Fish so that others can benefit from your work as well.
And since I needed to have a finer tunning over both the docker & the program's arguments, I have made a slightly more complex setup that I hosted here on Gist with Python if you want some inspirations.