A tool to automatically generate a NixOS config from a Docker Compose project.
Running a Docker Compose stack/project on NixOS is not well supported. One approach is to define a systemd service that runs docker-compose up on start and docker compose down on stop.
But with this approach, changes to individual services are not visible to NixOS, which means that NixOS will need to restart the systemd service on any change to the Compose file. This can be mitigated by defining a systemd reload handler, but it still is finicky to work with and will always remain opaque to NixOS.
To top it all off, using Docker Compose on NixOS is fairly redundant as the features you get with Compose are available natively on NixOS.
compose2nix takes your existing Docker Compose file(s) and converts each YAML service definition into a oci-container config. The tool also sets up systemd services to create all networks and volumes that are part of the Compose project. Since compose2nix uses the same library that the Docker CLI relies on under the hood, you also get Compose file validation and syntax checking "for free".
compose2nix supports setting additional systemd service and unit options through Docker Compose labels (search for the compose2nix.systemd. label in the samples).Install the compose2nix CLI via one of the following methods:
Run using nix run (recommended):
```
# Latest
nix run github:aksiksi/compose2nix -- -h
nix run github:aksiksi/compose2nix/v0.3.0 -- -h
nix run github:aksiksi/compose2nix/0c38d282d6662fc902fca7ef5b33e889f9e3e59a -- -h
2. Install from `nixpkgs`:
environment.systemPackages = [
pkgs.compose2nix
];
3. Add the following to your `flake.nix`:nix
compose2nix.url = "github:aksiksi/compose2nix";
compose2nix.inputs.nixpkgs.follows = "nixpkgs";
```
Optionally, you can pin to a specific version:
nix
compose2nix.url = "github:aksiksi/compose2nix/v0.3.0";
You can then install the package by adding the following to your NixOS config:
nix
environment.systemPackages = [
inputs.compose2nix.packages.x86_64-linux.default
];
Run compose2nix. Note that project must either be passed in or set in the Compose file's top-level "name".
compose2nix -project=myproject
By default, the tool looks for docker-compose.yml in the current directory and outputs the NixOS config to docker-compose.nix.
agenix works by decrypting secrets and placing them in /run/agenix/. To feed this into your Nix config:
my-env-file.env).compose2nix.compose2nix with the env file path(s) and set -include_env_files=true:compose2nix --env_files=/run/agenix/my-env-file.env --include_env_files=true
[!NOTE] If you also want to ensure that you only include env files in the output Nix config, set
-env_files_only=true.
The sops-nix integration allows you to reference secrets that are already configured in your NixOS system.
[!NOTE] This section assumes that:
sops-nixis already setup in your NixOS configuration- The secrets you want to use are already defined in your configuration
To use the sops-nix integration:
compose2nix.settings.sops.secrets label with comma-separated secret names to your Compose services:yaml
services:
webapp:
image: nginx:latest
labels:
- "compose2nix.settings.sops.secrets=example.env,some-folder/example-2.env"
compose2nix pointing to your encrypted secrets YAML file:bash
compose2nix \
--inputs docker-compose.yml \
--sops_file ./secrets/secrets.yaml
This will then generate a NixOS configuration that references your existing sops secrets as environment files. Note that they'll be appended to env files passed during connfig generation.
virtualisation.oci-containers.containers."webapp" = {
image = "nginx:latest";
# ...
environmentFiles = [
"/etc/existing-file.env" # passed in via CLI
# sops-nix secrets
config.sops.secrets."example.env".path
config.sops.secrets."folder/example-2.env".path
];
};
In this case, the project is called myproject and the service name is myservice. Replace podman with docker if using the Docker runtime.
sudo systemctl list-units podman-*
sudo systemctl list-units *myservice*
Note: if the Compose service has a container_name set, then the systemd service will not include the project name.
sudo systemctl restart podman-myproject-myservice.service
jq):sudo podman pull $(sudo podman inspect myproject-myservice | jq -r .[0].ImageName)
sudo systemctl restart podman-myproject-myservice.service
io.containers.autoupdate=registry label to each Compose service you want to have auto-updated.docker.io/repo/image). Otherwise, Podman will fail to start the container.sudo podman auto-update --dry-run to see which containers would get updated. Omit --dry-run to update & restart services.You can optionally enable a Podman-provided timer that runs the command above once per day at midnight (by default):
# Enable the existing timer unit.
systemd.timers."podman-auto-update".wantedBy = [ "timers.target" ];
See this page for details: https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html
By default, all generated services will be started by systemd on boot.
You can override this behavior in two different ways:
-auto_start=false.Disable or enable auto-start for a single service: Add a Compose label to your service like this:
yaml
services:
my-service:
labels:
# Enable
- "compose2nix.settings.autoStart=true"
# Disable
- "compose2nix.settings.autoStart=false"
docker compose downBy default, this will only remove networks.
sudo systemctl stop podman-compose-myservice-root.target
You can do one of the following:
-remove_volumes=truesudo podman volume prune to manually cleanup unreferenced volumesdocker compose upsudo systemctl start podman-compose-myservice-root.target
compose2nix has basic support for the Build spec. See [Supported Compose Features] below for details.
By default, a systemd service will be generated for each container build. This is a one-shot service that simply runs the build when started.
For example, if you have a service named my-service with a build set:
sudo systemctl start podman-build-my-service.service
Note that, until this is run, the container for my-service will not be able to start due to the missing image.
If you run the CLI with -build=true, the systemd service will be marked as a dependency for the service
container. This means that the build will be run before the container is started.
However, it is important to note that the build will be re-run on every restart of the root target or system. This will result in the build image being updated (potentially).
{
hardware.nvidia-container-toolkit.enable = true;
}
Docker only:
Make sure you are running Docker 25+:
{
virtualisation.docker.package = pkgs.docker_25;
}
devices or deploy (both map to the same thing under the hood):services:
myservice:
# ... other fields
# Option 1
devices:
- nvidia.com/gpu=all
# Option 2
deploy:
resources:
reservations:
devices:
# Driver must be set to "cdi" - all others are ignored.
- driver: cdi
device_ids:
- nvidia.com/gpu=all
# Required for Podman.
security_opt:
- label=disable
I always aim to support the latest stable version of NixOS (24.05 at the time of writing). As a result, some NixOS unstable options are not used.
If the option has a strong usecase, I am open to adding a CLI flag that can be deprecated once the option is stable.
systemd does not differentiate between a manual unit stop and a unit stopped due to a failure (i.e., in failed state). This means that if you stop a unit, it will automatically be started by the service(s) it depends on.
Suppose you have the following Compose file:
services:
app:
image: myname/app
depends_on:
- db
db:
image: postgres
If you manually stop the app systemd unit, the db unit will
*automatically restart it due to the UpheldBy setting.
Discussion with the systemd team: https://github.com/systemd/systemd/issues/35636
If you are using the Docker runtime and a Compose service connects to multiple networks, you'll need to use v25+. Otherwise, the container service will fail to start.
You can pin the Docker version to v25 like so:
{
virtualisation.docker.package = pkgs.docker_25;
}
Discussion: https://github.com/aksiksi/compose2nix/issues/24
internal networksFor some reason, when you run a rootful Podman container in a network that is marked as internal, port forwarding to the host does not work. Podman seems to completely isolate the network from the external world - including the host network! Note that Docker claims to support this behavior out-of-the-box (ref).
There is a workaround: remove the internal setting and set the network driver option no_default_route=1 (example).
networks:
my-network:
driver: bridge
driver_opts:
no_default_route: 1 # <<< This is what prevents external network access.
ipam:
config:
- subnet: 10.8.1.0/24
gateway: 10.8.1.0
This will allow you to connect from the host, while also preventing internet access from within the container.
This is where the check is done in Netavark: link
If a feature is missing, please feel free to create an issue. In theory, any Compose feature can be supported because compose2nix uses the same library as the Docker CLI under the hood.
services| Notes | ||
|---|---|---|
image |
✅ | |
container_name |
✅ | |
environment |
✅ | |
env_file |
✅ | |
volumes |
✅ | Short and long syntax supported. |
labels |
✅ | |
ports |
✅ | |
[dns](https://docs.dock |
—
$ claude mcp add compose2nix \
-- python -m otcore.mcp_server <graph>