Swiss army knife container for vscode development environments
Metadata | Value |
---|---|
Image | ghcr.io/xtruder/nix-devcontainer |
Image tags | v1,latest,edge |
Definition type | standalone or Docker Compose |
Works in Codespaces | Yes |
Container host OS support | Linux, macOS, Windows |
Languages, platforms | All languages that nix supports |
Contributors | @offlinehacker, @Rizary |
Maintainer | Jaka Hudoklin jaka@x-truder.net @offlinehacker |
Nix devcontainer is an opinionated vscode devcontainer that uses debian image for a base system and nix package manager for management of your development environments. Combination of a good base image and a best in class package manager, gives you versatile, reproduible and deterministic development environment that you can use everywhere.
-
Debian slim docker image
Docker base image, which provides minimalistic environment in which both nix and vscode remote extension can run without any issues.
-
Used for providing declarative, deterministic and reporoducible development environment that you can run anywhere. Imagine better conda alternative. You write a single
shell.nix
file that describes your environment and all your tools and vscode extensions will be running with same exact versions of binaries, same environment variables, same libraries forever. -
Nix Environment Selector vscode extension
Used to automatically load nix development environment into your vscode and provides capabilities to reload it later when environment changes, without having to rebuild docker image from scratch on every change.
-
Direnv shell environment loader
While nix environment loader extension loads environment for vscode, you want
direnv
to manage you shell environment.Direnv
loads nix environment (defined byshell.nix
file) into your shell and reloads it automatically when it changes, keeping your environment fresh.
There are sevaral example templates you can use to quickly bootstrap your project:
-
Example project using
nix-devcontainer
for golang development, with docker-compose running docker-in-docker service for building docker images. -
nix-devcontainer-python-jupyter
Example project using
nix-devcontainer
for python and jupyter notebooks, with python packages managed by nix.
If this is your first time using a development container, please follow the getting started steps to set up your machine, install docker and vscode remote extensions.
Make sure that your project has shell.nix
that describes your development
environment. Internally nix environment selector vscode extension runs
nix-shell
to configure vscode's development environment.
Here is minimal example of shell.nix
to get you started:
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
# nativeBuildInputs is usually what you want -- tools you need to run
nativeBuildInputs = with pkgs; [
#hello
];
}
If you want nix-shell
to automatically run for your shell environemnts
running in your development container, create .envrc
file.
A minimal example of .envrc
file:
use_nix
For more informattion on how to develop with nix-shell
you can take a
look here: https://nixos.wiki/wiki/Development_environment_with_nix-shell
Integrating devcontainer
into your project is as simple as creating devcontainer.json
file and a
Dockerfile
in .devcontainer
directory.
Example .devcontainer/devcontainer.json
:
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at
// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/docker-existing-dockerfile
{
"name": "devcontainer-project",
"dockerFile": "Dockerfile",
"context": "${localWorkspaceFolder}",
"build": {
"args": {
"USER_UID": "${localEnv:USER_UID}",
"USER_GID": "${localEnv:USER_GID}"
},
},
// run arguments passed to docker
"runArgs": [
"--security-opt", "label=disable"
],
"containerEnv": {
// extensions to preload before other extensions
"PRELOAD_EXTENSIONS": "arrterian.nix-env-selector"
},
// disable command overriding and updating remote user ID
"overrideCommand": false,
"userEnvProbe": "loginShell",
"updateRemoteUserUID": false,
// build development environment on creation, make sure you already have shell.nix
"onCreateCommand": "nix-shell --command 'echo done building nix dev environment'",
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
// select nix environment
"arrterian.nix-env-selector",
// extra extensions
//"fsevenm.run-it-on",
//"jnoortheen.nix-ide",
//"ms-python.python"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "go version",
}
Example .devcontainer/Dockerfile
:
FROM ghcr.io/xtruder/nix-devcontainer:v1
Dockerfile is needed for build triggers to run. Build triggers will change
user uid
and gid
to one provided by USER_UID
and USER_GID
env variables
and change ownersip of /nix
and /home
folders. This is required, as
docker currently does not provide a way to map filesystem uids/gids.
If you don't need this and your host user always has 1000:1000
uid/gid,
you can also specify image directly by setting image
parameter
in devcontainer.json
If you already have your shell.nix
, you can also set to use it in your
project .vscode/settings.json
file:
{
"nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix",
}
Alternatively you can use docker-compose
instead. This allows you to run multiple
services and have more control over development environment. In this case you need
to specify path to compose file in your devcontainer.json
file. Compose file
can also be in your project root if you prefer.
Example .devcontainer/devcontainer.json
:
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at
// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/docker-existing-dockerfile
{
"name": "devcontainer-project",
"dockerComposeFile": "docker-compose.yml",
"service": "dev",
"workspaceFolder": "/workspace",
"userEnvProbe": "loginShell",
"updateRemoteUserUID": false,
// build development environment on creation
"onCreateCommand": "nix-shell --command 'echo done building nix dev environment'",
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
// select nix environment
"arrterian.nix-env-selector",
// extra extensions
//"fsevenm.run-it-on",
//"jnoortheen.nix-ide",
//"ms-python.python"
],
}
Example .devcontainer/docker-compose.yml
:
version: '3'
services:
dev:
build:
context: ../
dockerfile: .devcontainer/Dockerfile
args:
USER_UID: ${USER_UID:-1000}
USER_GID: ${USER_GID:-1000}
environment:
# list of docker extensions to load before other extensions
PRELOAD_EXTENSIONS: "arrterian.nix-env-selector"
volumes:
- ..:/workspace:cached
security_opt:
- label:disable
network_mode: "bridge"
When you open a project vscode should ask you to open project in remote container. If you click to open in remove container, dev environment should be built automatically and after you should be ready to start coding.
Alternativelly you can choose Remote-Containers: Reopen Folder in Container from command menu.
If opening a workspace, please make sure you open workspace in devcontainer by selecting **Remote-Containers: Open workspace in Container". Remote containers extension does not currently provide a popup for opening a workspace, but will only provide you with an option to open a project in devcontainer or to open workspace locally, but this is not what you want.
If running under linux and uid
or gid
of user running vscode
are not equal to 1000:1000
,
you will have to set USER_UID
and USER_GID
environment variables.
These should be set globally or in a shell you are running code
command from. vscode
inherits environment from where it is started, but make sure these environment variables
are set or files will have incorrect permissions.
This is a list of nix-devcontainer
image build arguments and its defaults:
Name | Description | Default |
---|---|---|
USERNAME | Username of the user in container | code |
USER_UID | ID of the user in container | 1000 |
USER_GID | Group ID of the user in container | 1000 |
After updating shell.nix
with changes that affect your development environment.
If you are using nix environment selector extension you can choose Nix-Env: Hit environment
from command menu and it will rebuild your environment and after that it will ask you
to reload your editor.
Alternatively you can also reload your editor by choosing Developer: Rebuild Container
and it will rebuild your environment on editor start.
If you want to automatomatically rebuild your environment when shell.nix
changes
you can use fsevenm.run-it-on
extension and choose to automatically run
nixEnvSelector.hitEnv
command. Here is an example of settings you can put in your
workspace settings (.vscode/settings.json
file in your project.)
{
"runItOn": {
"commands": [
{
"match": "flake\\.nix",
"isShellCommand": false,
"cmd": "nixEnvSelector.hitEnv"
}
]
}
}
VSCode remote containers have internal support for cloning dotfiles repo and running custom install script when devcontainer is opened. Check how to personalize your environment here. An example of dotfiles repository is available here.
If you need to add another service, please make sure you are using docker-compose
and not a plain
devcontainer.json
, since it does not support running multiple services.
You can add other services to your docker-compose.yml
file as described in Dockers documentation. However, if you want anything running in these service to be available
in the dev container on localhost, or want to forward the service locally,
be sure to add this line to the docker-compose
service config:
# Runs the service in the same network namespace as the dev container,
# keeping services on localhost makes life easier
network_mode: service:app
This will make sure your dev container and service containers are running in same network namespace.
Caching nix store is as simple as adding named docker volume on /nix
.
FROM ghcr.io/xtruder/nix-devcontainer:v1
VOLUME /nix
Then add named docker volume to devcontainer.json
or docker-compose.yml
as explained
in next paragraph.
You can cache additional directories using docker volumes. You will need to make sure volumes
have right permissions. To do that you need to first create directory in your Dockerfile
and
set it as a volume and later put named volume in your docker-compose.yml
or in devcontainer.json
.
-
Create directory and add volume to your
Dockerfile
RUN mkdir -p /home/${USERNAME}/.cache VOLUME /home/${USERNAME}/.cache
-
Add named volume to
devcontainer.json
ordocker-compose.yml
:{ "name": "project-name", //..., "mounts: [ "source=project-name_devcontainer_home-cache,target=/home/user/.cache,type=volume" ] }
Alternatively If you are using
docker-compose
you can add the following to yourdocker-compose.yml
in.devcontainer
directory. This will mount named volume to your desired location.version: '3' services: dev: ... volumes: - home-cache:/home/user/.cache volumes: home-cache
There are several ways how to use docker inside devcontainer. The easiest is just
to mount docker.sock
in devcontainer:
-
Add mount in
docker-compose.yml
ordevcontainer.json
:version: '3' services: dev: ... volumes: - type: bind source: /var/run/docker.sock target: /var/run/docker.sock
{ //..., "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ] }
-
(Optional) Add user to docker group in your
Dockerfile
ARG DOCKER_GID=966 RUN groupadd -g ${DOCKER_GID} docker && usermod -a -G docker ${USERNAME}
Be aware that exposing docker socket to your development environment is a security risk, as it exposes your system to potentially malicious development environment. Make sure you never run untrusted code in such environment.
Better alternative is to run rootless docker in docker as separate privileged service via docker-compose
. While this service runs
in privileged container, it mittigates the risk by running docker
without root.
version: '3'
services:
# your development container
dev:
...
# it's advised to use shared network namespace, as it simplifies
# development and can allow connections to docker via localhost
network_mode: "service:docker"
docker:
image: docker:dind-rootless
environment:
DOCKER_TLS_CERTDIR: ""
DOCKER_DRIVER: overlay2
privileged: true
volumes:
- ..:/workspace:cached
- docker:/var/lib/docker
security_opt:
- label:disable
network_mode: bridge
volumes:
docker:
Only linux kernels of version 5.11+ support overlay2 storage driver in rootless containers. You can use default vfs or fuse-overlayfs drivers, but both are a bit slow, so they are not recommended
Some vscode extensions have issue that development environment is loaded too late. Currently vscode does not support an option to make extensions load after some other extension, but only supports extension dependencies, where one extension can wait for other extension to load, see also microsoft/vscode#57481.
To workaround this issue we have implemented a hack, a vscode extension preloader
that modifies extensions package.json
on the fly to make extensions depend on
other set of extensions while they are installed, but before they are loaded.
You can enable this feature/hack by setting PRELOAD_EXTENSIONS
environment
variable in your devcontainer.json
or docker-compose.yml
.
Example devcontainer.json
settings:
{
//...
"containerEnv": {
"PRELOAD_EXTENSIONS": "arrterian.nix-env-selector"
},
//...
"extensions": [
"arrterian.nix-env-selector",
//...
]
}
Running this devcontainer with Podman is currently not supported, due to vscode remote
containers not supporting passing build flags to podman
and podman-compose
.
See also microsoft/vscode-remote-release#3545, there is also
a possible workaround, but i haven't tried it yet.
- Vscode starts devcontainer from
devcontainer.json
definition .devcontainer/Dockerfile
(that inherits from this image) is used to build development container, severalONBUILD
docker triggers are run to change useruid
andgid
of non-root user (code
by default) in container and fix ownership of files.- Upon start vscode installs extensions defined in
devcontainer.json
, includingarrterian.nix-env-selector
. arrterian.nix-env-selector
extension evaluates development shell defined inshell.nix
file and sets vscode environment variables based on that environment.- All other extensions are loaded into vscode.
- When you start vscode terminal, environment variables are not automatically inherited
from
vscode
, that's why this devcontainer setsdirenv
shell hook to automatically load environment into your shell.
It's recommended to open this project in vscode devcontainer or via github codespaces. This will automatically prepare development environment with all required dependencies.
examples
- devcontainer examplessrc
- image sourcetest
- image smoke test
For basic validity testing you should run make test
, which will build test
image and runs tests in image. Sanity checks are run by first running
direnv hook which loads nix environment and then it sources test.sh
script,
which does a few basic sanity checks.
You should also check if image works with example templates.
Copyright (c) X-Truder. All rights reserved.
Licensed under the MIT License. See LICENSE.