A Devcontainer for Elixir
Hey folks! First, a joke:
What’s the worst vegetable to bring on a boat?
A leek
Now, let's talk about devcontainers for Elixir.
The Problem
Claude now writes 95% of my code. I've been lukewarm about the whole "AI" hype until Opus 4.5 was released. It was a game-changer for me. Sonnet was alright for small stuff with very guided instructions and it was a small net benefit for me, but Opus 4.5 was the first good model in my opinion. It's still not perfect and probably will never be. At least I feel comfortable giving it less specific instructions knowing that it will produce something slightly better than absolute dogshit.
This comfort allowed me to point Opus at larger and more complex problems, like migrating a Ruby library into Elixir or building a complex UI and generating its database model too.
I wanted to let Opus "do its thing" without me having to click "Yes, Yes, Yes" all the time, but I also wanted to avoid an OpenClaw moment like Opus uploading my SSH keys to GitHub. Claude comes with the --dangerously-skip-permissions flag which allows the agent to "run free". Some call it the YOLO or "Danger" mode because it might break your computer or upload your secrets to the web.

So, I knew that I needed to create a safe environment for Opus to run in and devcontainers seemed to provide exactly that.
The Solution
Devcontainers have been around for a while, but I think they were used primarily to spin up dev environments in the cloud. Now, with the advent of "agentic programming", they've come in handy to separate the agent from the host system.
In essence, devcontainers are just Docker containers that you spin up locally and run your agent in. That's it.
Starting a Docker container is easy. The tricky part is to restrict the agent to only the things it actually needs.
For example, it should not follow links to nefarious sites and download their malware. Or it should never post anything to Pastebin. Also, it should not read the secrets in your .env files or remove your root directory.
Now, let's dive into how I ended up building my own devcontainer for Elixir.
The Setup
I started by searching for existing devcontainers for Elixir/Phoenix, but found nothing specific. So, I looked at Anthropic's example devcontainer and that became the starting point to build my own.
You can find the final devcontainer I use every day on GitHub.
The base of my devcontainer was Anthropic's Dockerfile, but I made the following changes to it:
- Replace the
nodebase image with Hex.pm's Elixir/Erlang image - Run the container as a non-root user
- Install Tidewave by default
This gave me a more secure and Elixir-based devcontainer, but it was still missing three things: A better firewall, protected paths, and CLI commands.
The Firewall
Anthropic's devcontainer restricts outgoing network traffic to a list of IPs that are resolved when you build the container. These hard-coded IPs don't work well if the domain you want to connect to uses a load-balancer or servers on multiple IPs (like claude.ai). At build-time, Docker might resolve a domain to one IP, but when Claude runs the HTTP request, it might connect to a different IP and the request gets blocked.
I decided to replace this flaky setup with domain-based traffic restrictions. So, instead of allow-listing individual IPs, I'd allow-list entire domains instead. Setting this up was surprisingly difficult and took the most time when creating the devcontainer.
I tried out a multitude of proxies like TinyProxy and 3Proxy, but none of them handled both SSL and non-SSL traffic and domain-level filtering well. I ended up using Squid because, although it is overpowered, it worked well without too much configuration.
Using Squid and a little bit of bash, I was able to define the allow-list of domains in a single .txt file and Squid would restrict all outgoing traffic to only these domains, blocking everything else.
Protected Paths
Next, I wanted to make it easy to "hide" files like .env from Claude. Even if you tell Claude that it MUST never read them, it can and will still do it. The best option would be to remove them from my project entirely, but that's also not practical since I needed these secrets.
I ended up mounting empty files and folders over the list of protected paths in the devcontainer. This way Claude still sees the files, but they are empty, at least in the container. I also added the protected paths as deny rules to Claude's project-wide settings.json file. This won't stop Claude from listing or even reading the files, but at least it will try less often.
I inject the needed env variables into the container, so Claude could still run echo $MY_SECRET and get the secret. That's why even with all these layers of protection, I make sure to never use production secrets in my .env file.
CLI Commands
Lastly, I added a Makefile with shortcuts for the commands I use often, like to start, stop, or rebuild the devcontainer, to start Tidewave, print out logs, or start the Phoenix server. This makes developing with the devcontainer a bit smoother.
Conclusion
And that's it! I hope you enjoyed this article! If you want to use the devcontainer or extend it, you can find it on GitHub.
If you want to support me, you can buy my book or video courses (one and two). Follow me on Bluesky or subscribe to my newsletter below if you want to get notified when I publish the next blog post. Until next time! Cheerio 👋