Safe, isolated development environments for trying out projects without risking your machine.
Key features:
- Runs each project in an isolated Docker container
- CLI-based development: nvim, AI agents, etc.
- Transparent port forwarding: access apps in your browser as if they were running on the host
- Scripts are easy to audit: less than 300 lines of bash running on the host
- Minimal dependencies: docker, docker-compose, git, bash, ssh
As a side effect, you get clean, reproducible, and easy-to-reset development environments.
No more npm install on your host, no AI agents reading your files.
Safely experiment with any project.
Simple, transparent scripts you can easily audit:
- Main script
bin/dev: ~200 lines of bash, mostly if/else and case statements, linear, no recursion - Port forwarding
bin/ports: ~50 lines of bash, starting a ssh tunnel withssh -L ... - Docker-compose file: ~30 lines of yaml, defining services and volumes
- Dockerfile: ~20 lines, defining the base image and which tools to install by default
- Setup scripts in
setup/: ~5-30 lines of bash each
You are supposed to modify the Dockerfile and setup scripts to fit your needs. The docker-compose file can also be used to include additional services or enable permanent port forwarding.
Requires docker, docker-compose and some basic tools like git, bash and ssh.
The command bin/dev should be somewhere in your PATH.
mkdir -p ~/bin
wget https://raw.githubusercontent.com/f0i/dev/refs/heads/main/bin/dev -O ~/bin/dev
echo "export PATH=\"$PATH:$HOME/bin\" >> ~/.bashrc"
All other files will be added per project as needed:
- Assuming a project called
arenain~/projects/arena - Clone this repo into the project root
dev arena init - During init, you'll be prompted to:
- Copy your SSH public key and
.gitconfig(choose yes to copy tosetup/user/, or no to configure manually later) - Clone a nvim configuration (automatically detects your
~/.config/nvimgit remote, or uses default)
- Copy your SSH public key and
- Modify the
Dockerfileto include the services and tools you need - Build the docker container using the command
dev build - Start the docker container with
dev start - Connect to the container with
devand start development - Enable port forwarding to the host
dev ports 3000 8080:80, in this case port 3000 and 8080 of the host will be forwarded to port 3000 and 80 of the container
Pull first, then apply. dev upgrade runs git pull here and in nvim/, and stops there. It does not build or restart anything.
What you need after that depends on what changed.
| Changed | Command |
|---|---|
Dockerfile, setup/*.sh |
dev build, then dev start |
docker-compose.yml |
dev start |
bin/dev, bin/ports |
nothing, they run on the host |
dev start is docker-compose up -d, not docker-compose start.
It recreates the container when the compose config or the image changed, and leaves it alone otherwise, so it is safe to run at any time.
It never builds: skip dev build and it will happily keep the old image.
Recreating replaces the running container, and anything live inside it goes too: shells, tmux sessions, background processes. The /workspace bind survives, the home directory does not.
Getting newer packages. An unchanged Dockerfile means every layer is a cache hit, so a plain dev build is fast and changes nothing. That also means it does not pick up new versions: a RUN layer is cached on the command string, not on what the command would download, so curl … | bash installers stay at whatever version they first built. Two separate needs:
dev build --no-cache # re-run every step: new versions of curl-installed tools
dev build --pull # newer base image: distribution package updates
Arguments are passed through to docker-compose, so the two combine.
setup/agent.sh installs Claude Code, opencode and the Gemini CLI.
Comment out the RUN /setup/agent.sh line in the Dockerfile to leave them out, or edit the script to change the set.
setup/user/claude/settings.json is copied to ~/.claude/ and sets three defaults for Claude Code:
- no attribution on commits or pull requests, and no session link
- Remote Control off at startup, so a session is reachable from outside only after you run
/remote-control - approval required before Claude messages one of your sessions on another machine, which is the case that travels through Anthropic's servers
Edit that file to change them. Claude Code writes its own settings there too, so a container keeps whatever you change inside it until it is rebuilt.
During dev init, the nvim configuration is cloned into ./dev/nvim and mounted at /home/node/.config/nvim inside the container.
Why not mount ~/.config/nvim directly?
- Isolation: Each project gets its own nvim config for reproducibility
- No modifications: Prevents the container from modifying your personal config files
- Different needs: Container setup might need different plugins/settings than your host
Using a different nvim config:
- During init, it auto-detects the git remote of your
~/.config/nvimif it exists - Or you can manually clone any nvim config repo to
./dev/nvimbefore building - Or skip the nvim setup during init and add it later
MIT License. See LICENSE.md for details.