Hello reader. I have been trying to replicate the dev container experience that I had when I was still using vscode (I use neovim now btw) but it wasn't quite that, I tried some neovim plugins but didn't really like it. But I'm happy to tell you that I finally got something, just for Golang apps, but we are getting there. Let's quickly see how I achieved this.
You will need docker and docker compose installed on your system. I use Manjaro so for me installing these two was as easy as running
pamac install docker docker-compose
Once you have these two installed you will need a text editor, I will be using neovim. What I mean by the "dev container experience" is to be able to have my code on my host, but run and debug it inside a container so that way my computer doesn't have any dependencies and I can easily reproduce the dev environment on another PC without having a "it worked on that other machine" moment. As exemple we will build a simple Go app that returns "Hello world" upon receiving a GET request at "/hello".
After running go mod init app-name
we create our main.go and write the following code inside
So this is a simple Go server using the standard library. We want to run the app in a container so we will create a docker-compose.yml
at the root of our directory with the following content
Let's go through what the content of this file mean. In case you are not familiar with docker-compose I recommend you take a look at docker first and understand the concept of containerization and then read docker-compose docs. For the rest of the post I will assume you have a basic understanding of docker-compose. Nothing crazy here we just created a container based on Go's official image and we map the port 8080 of that container to the port 8080 on our host. The important part comes from the volumes section, we mount our current directory into the container so that we can have access to our files from inside it. If we run this using docker compose up
We see that the container exists automatically, that's normal because the container doesn't have any process running. A way to solve that would be to give it a command to run.
Setting the command as ["tail", "-f", "/dev/null"]
means that the container will continuously run the tail command to follow the content of /dev/null, effectively doing nothing but keeping the container running indefinitely. Now if we run docker compose up
again we get this
Let's run it again with docker compose up -d
.
Now that our container is running in the background we can go inside it and run our app. We named our container server
so we will run docker exec -it server bash
.
We are able to build and run our code from the container. And we can test this using curl
So this is really simple, but one limitation I found here is when we bring dependencies into the code, let's say we want to rewrite our server using Chi. Our code would now look like this
Then we can go into our container and run it
You can see that we first ran go get -u github.com/go-chi/chi/v5
to download chi and then we were able to build and run the code. All good. But if you take a look into our editor
We see that our LSP is not happy, even tho we downloaded chi, our editor can not find it. And that is completely normal because the dependencies downloaded with go get
are stored in the GOPATH
in our container so our LSP doesn't have access to them. To solve that and still get LSP support and completions in our editor we can download chi on our machine and the errors will go away but the whole point of working in a dev container is to not have to install anything on our machine. The way I solved this issue was by using go mod vendor
. With go mod vendor you can download all the dependencies required to build your app in a folder and you can even share that folder with your team mates or push it to source control. That way even if you need to work on the app but you have no access to internet, Go will take the dependencies from the vendor directory. You can read more about go vendor on Go's official documentation.
All we have to do is run go mod vendor
inside our container
And on the screenshot you can see that after we ran the command it created a new vendor
directory inside our package root folder. And inside that folder you have a folder named github.com
which contains the code for the chi library. There is also a modules.txt
file that contains the list of all the dependencies. Now if we restart our LSP on our host machine we can see that the LSP will be happy now
The LSP is configured to take dependencies from the vendor folder if present in your codebase. Now the dependencies are installed in our container, the vendor folder keeps a copy of these dependencies so that we can still have our LSP support without having to install the same packages on our host machine.
As a beginner Go developer and docker user myself I am so excited to learn more about these tools, Go is incredible because it has so much tools builtin already and docker is just awesome. I believe that with much more understanding of how volumes and LSPs work I can make this setup even better and why not turn it into a tool that could spin up automatically dev containers and connect to it from neovim, we'll see. Either way I will keep learning Go and Docker. Thanks for reading :) if you have any remarks please let me know in the comments. All the code showed in this post is available on this GitHub repo
Peace
Insightful !