Skip to content
Back to skills

Docker Compose To Nixos

ASecurity

Converts Docker Compose configurations to NixOS modules using the dendritic pattern with Arion. Creates modules with system users, sops secrets, Arion docker-compose config, and Tailscale integration. Use when converting docker-compose.yaml files to NixOS modules or creating new Arion-based services.

  • 666 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added February 8, 2026
developmentbashsqlnodedockergitapidatabasebackend

Works with

  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill docker-compose-to-nixos --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Docker Compose To Nixos?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Docker Compose To Nixos
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-docker-compose-to-nixos/badge)](https://www.skillsdirectory.com/skills/majiayu000-docker-compose-to-nixos)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: docker-compose-to-nixos
description: Converts Docker Compose configurations to NixOS modules using the dendritic pattern with Arion. Creates modules with system users, sops secrets, Arion docker-compose config, and Tailscale integration. Use when converting docker-compose.yaml files to NixOS modules or creating new Arion-based services.
---

# Docker Compose to NixOS Module

This skill converts Docker Compose configurations to NixOS modules following the dendritic pattern used in this repository.

## Dendritic Pattern Overview

The dendritic pattern organizes modules by **aspect** (functionality) rather than **class** (nixos/home/shared):
- File: `modules/services/service-name.nix` (NOT `modules/nixos/services/service-name.nix`)
- Contains both nixosModules and homeModules in the same file
- Flake inputs defined in module files, materialized with `nix run '.#write-flake'`
- Every module MUST have a unique `key` attribute for deduplication

## Conversion Workflow

Follow this checklist when converting a docker-compose.yaml:

- [ ] **1. Analyze docker-compose.yaml**: Identify services, volumes, networks, secrets
- [ ] **2. Determine UID/GID allocation**: Add to `inventory/users-groups.nix` if system user needed
- [ ] **3. Create module skeleton**: Use template in this document
- [ ] **4. Add system user/group**: Reference inventory values with flakeConfig
- [ ] **5. Configure sops secrets**: Use hierarchical naming (e.g., `service/secret-name`)
- [ ] **6. Create sops template**: Environment file with placeholders
- [ ] **7. Set up directories**: systemd tmpfiles.rules
- [ ] **8. Convert services**: Map docker-compose to Arion configuration
- [ ] **9. Add Tailscale serve** (optional): For external access
- [ ] **10. Stage with git add**: Flakes only see committed/staged files
- [ ] **11. Validate**: Run `just nixOpts= eval-nixos` in a loop until fixed
- [ ] **12. Add secrets**: Use `sops set` commands to populate secret values
- [ ] **13. Run comprehensive validation**: `just eval-all`
- [ ] **14. Format**: `just lint` (may reformat, amend if needed)
- [ ] **15. Commit changes**: Follow git workflow in CLAUDE.md

## Module Template

```nix
{
  inputs,
  self,
  config,
  ...
}:
let
  flakeConfig = config;
in
{
  flake.nixosModules.service-name =
    { config, lib, ... }:
    let
      inherit (lib) mkDefault;
    in
    {
      key = "nixos-config.modules.nixos.service-name";

      # Explicit imports for all dependencies
      imports = [
        inputs.sops-nix.nixosModules.sops
        inputs.arion.nixosModules.arion
        self.nixosModules.tailscale
        self.nixosModules.inventory
      ];

      config = {
        # User/group creation
        users.groups.service-name = {
          gid = flakeConfig.inventory.usersGroups.systemUsers.service-name.gid;
        };

        users.users.service-name = {
          isSystemUser = true;
          group = "service-name";
          uid = flakeConfig.inventory.usersGroups.systemUsers.service-name.uid;
        };

        # Sops secrets
        sops.secrets."service-name/secret1" = { };
        sops.secrets."service-name/secret2" = { };

        # Sops template for environment variables
        sops.templates."service-name-env".content = ''
          SECRET1=${config.sops.placeholder."service-name/secret1"}
          SECRET2=${config.sops.placeholder."service-name/secret2"}
          PUID=${toString flakeConfig.inventory.usersGroups.systemUsers.service-name.uid}
          PGID=${toString flakeConfig.inventory.usersGroups.systemUsers.service-name.gid}
        '';

        # Directory structure
        systemd.tmpfiles.rules = [
          "d /mnt/service-name 0755 service-name service-name -"
          "d /mnt/service-name/data 0755 service-name service-name -"
        ];

        # Arion docker-compose configuration
        virtualisation.arion.backend = "docker";
        virtualisation.arion.projects.service-name = {
          serviceName = "service-name-docker-compose";
          settings = {
            services = {
              main-service = {
                service = {
                  image = "namespace/image:pinned-version"; # NEVER use :latest
                  container_name = "service-name";
                  restart = "unless-stopped";
                  ports = [ "3000:3000" ];
                  volumes = [
                    "/mnt/service-name/data:/data"
                  ];
                  env_file = [
                    config.sops.templates.service-name-env.path
                  ];
                };
              };
            };
          };
        };

        # Tailscale serve (optional)
        services.tailscale.serve = {
          enable = mkDefault true;
          services.service-name = {
            serviceName = "service-name";
            protocol = "https";
            target = "localhost:3000";
          };
        };
      };
    };
}
```

## Key Patterns

### 1. System User Management

**Add to `inventory/users-groups.nix`:**
```nix
{
  systemUsers = {
    existing-service = { uid = 2001; gid = 2001; };
    new-service = { uid = 2002; gid = 2002; }; # Next sequential
  };
}
```

**Reference in module with flakeConfig:**
```nix
{ inputs, self, config, ... }:
let
  flakeConfig = config;  # CRITICAL: Access flake-level data
in
{
  flake.nixosModules.service-name = { config, lib, ... }: {
    config = {
      users.groups.service-name = {
        gid = flakeConfig.inventory.usersGroups.systemUsers.service-name.gid;
      };
      users.users.service-name = {
        isSystemUser = true;
        group = "service-name";
        uid = flakeConfig.inventory.usersGroups.systemUsers.service-name.uid;
      };
    };
  };
}
```

**Why flakeConfig?** The outer `config` parameter is at the flake level (same level as `self`, `inputs`). The inner `config` parameter inside `flake.nixosModules.service-name` is at the NixOS module level. Use `flakeConfig` to access flake-level inventory data, matching the pattern in `modules/lxc.nix` and `modules/inventory.nix`.

### 2. Sops Secrets Management

**Hierarchical naming with `/`:**
```nix
sops.secrets."service-name/nextauth-secret" = { };
sops.secrets."service-name/database-password" = { };
sops.secrets."service-name/api-key" = { };
```

**Create template for environment file:**
```nix
sops.templates."service-name-env".content = ''
  NEXTAUTH_SECRET=${config.sops.placeholder."service-name/nextauth-secret"}
  DATABASE_URL="postgresql://user:${config.sops.placeholder."service-name/database-password"}@host/db"
  API_KEY=${config.sops.placeholder."service-name/api-key"}
  PUID=${toString flakeConfig.inventory.usersGroups.systemUsers.service-name.uid}
  PGID=${toString flakeConfig.inventory.usersGroups.systemUsers.service-name.gid}
'';
```

**IMPORTANT: SOPS YAML Structure**

Secrets with `/` in Nix (like `service-name/secret`) MUST be structured as nested YAML:

```yaml
# CORRECT - nested structure
service-name:
  nextauth-secret: value
  database-password: value
  api-key: value

# WRONG - flat structure (will cause build errors)
service-name/nextauth-secret: value
service-name/database-password: value
```

**Add actual secrets with sops set (using nested path):**
```bash
sops set secrets/docker-on-nixos/secrets.yaml '["service-name"]["nextauth-secret"]' "$(echo '"'$(apg -x16 -m16 -MLCN -n1)'"')"
sops set secrets/docker-on-nixos/secrets.yaml '["service-name"]["database-password"]' "$(echo '"'$(apg -x16 -m16 -MLCN -n1)'"')"
sops set secrets/docker-on-nixos/secrets.yaml '["service-name"]["api-key"]' '""'  # Empty for manual population
```

### 3. Directory Structure

**Mount strategy:**
- `/mnt/service-name/data` - Application data (persistent)
- `/mnt/service-name/var/component` - Per-component state

**Example:**
```nix
systemd.tmpfiles.rules = [
  "d /mnt/service-name 0755 service-name service-name -"
  "d /mnt/service-name/data 0755 service-name service-name -"
  "d /mnt/service-name/var 0755 service-name service-name -"
  "d /mnt/service-name/var/database 0755 service-name service-name -"
  "d /mnt/service-name/var/cache 0755 service-name service-name -"
];
```

### 4. Docker Compose Service Translation

**Docker Compose:**
```yaml
services:
  app:
    image: namespace/app:1.2.3
    container_name: myapp
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - ./data:/app/data
    environment:
      SECRET: ${SECRET}
    depends_on:
      - database
```

**Arion Configuration:**
```nix
services = {
  app = {
    service = {
      image = "namespace/app:1.2.3";  # ALWAYS pin version
      container_name = "myapp";
      restart = "unless-stopped";
      ports = [ "3000:3000" ];
      volumes = [
        "/mnt/service-name/data:/app/data"
      ];
      env_file = [
        config.sops.templates.service-name-env.path
      ];
      depends_on = [ "database" ];
    };
  };
};
```

**Key differences:**
- `ports: ["3000:3000"]` - Array of strings
- `volumes: ["/host:/container"]` - Absolute host paths, array format
- `env_file: [path]` - Reference sops template path
- `depends_on: ["service"]` - Array of service names
- `expose: ["7700"]` - For internal-only ports
- **NEVER use `:latest`** - Always pin to specific version

### 5. Tailscale Serve Integration

**Basic pattern:**
```nix
services.tailscale.serve = {
  enable = mkDefault true;
  services.service-name = {
    serviceName = "service-name";  # Will be svc:service-name
    protocol = "https";
    target = "localhost:3000";
  };
};
```

This exposes the service at `https://svc:service-name` on the Tailscale network.

## Image Version Pinning

**CRITICAL:** Never use `:latest` tag. Always pin to specific versions:

- ✅ `ghcr.io/karakeep-app/karakeep:0.29.1`
- ✅ `getmeili/meilisearch:v1.13.3`
- ✅ `gcr.io/zenika-hub/alpine-chrome:124`
- ❌ `archivebox/archivebox:latest`
- ❌ `postgres:latest`

Find current versions:
- Check project's GitHub releases
- Check Docker Hub/GHCR tags
- Use `docker pull image:latest && docker inspect image:latest` to find current digest

## Validation Workflow

**Iterative validation:**
```bash
# Fast iteration on current machine
just nixOpts= eval-nixos

# Fix errors, repeat until clean

# Comprehensive validation (all 12 configs, ~45s)
just eval-all

# Format and fix any linting issues
just lint

# If lint changed files, amend commit
git add . && git commit --amend --no-edit
```

## Common Patterns

### Multiple Services in One Project

```nix
virtualisation.arion.projects.service-name = {
  serviceName = "service-name-docker-compose";
  settings = {
    services = {
      # Main app
      app = {
        service = {
          image = "namespace/app:1.0.0";
          depends_on = [ "database" "cache" ];
          # ...
        };
      };

      # Database
      database = {
        service = {
          image = "postgres:16.1";
          expose = [ "5432" ];  # Internal only
          volumes = [
            "/mnt/service-name/var/database:/var/lib/postgresql/data"
          ];
          environment = {
            POSTGRES_PASSWORD = config.sops.placeholder."service-name/db-password";
          };
        };
      };

      # Cache
      cache = {
        service = {
          image = "redis:7.2.3";
          expose = [ "6379" ];
        };
      };
    };
  };
};
```

### Environment Variables vs env_file

**Use env_file for secrets:**
```nix
env_file = [
  config.sops.templates.service-name-env.path
];
```

**Use environment for non-sensitive config:**
```nix
environment = {
  NODE_ENV = "production";
  PORT = "3000";
  MEILI_NO_ANALYTICS = "true";
};
```

### Container Commands

```nix
service = {
  image = "namespace/app:1.0.0";
  command = [
    "schedule"
    "--foreground"
    "--update"
    "--every=day"
  ];
};
```

## Git Workflow

**Critical for flakes:**
1. Stage new files immediately: `git add modules/services/service-name.nix inventory/users-groups.nix`
   - Flakes only see committed or staged files
2. Validate before committing
3. Check for existing unpushed commits - consider using `--fixup` if related
4. Follow commit message format in CLAUDE.md

## Troubleshooting

**"flake is dirty" warning**: Normal during development, flake sees uncommitted changes

**Evaluation fails with "attribute missing"**:
- Check if module is imported in machine configuration
- Verify inventory.usersGroups exists if using it
- Ensure all imports (sops-nix, arion) are available

**UID/GID conflicts**: Check `inventory/users-groups.nix` for next available ID

**Secrets not decrypted**: Ensure sops secrets file exists and is properly encrypted for the target machine's key

## Examples and References

- **Detailed examples**: See [EXAMPLES.md](EXAMPLES.md) for complete archivebox and karakeep conversions
- **Technical reference**: See [REFERENCE.md](REFERENCE.md) for docker-compose to Arion mapping tables and advanced patterns

## Quick Reference

**File locations:**
- Module: `modules/services/service-name.nix`
- UID/GID: `inventory/users-groups.nix`
- Secrets: `secrets/docker-on-nixos/secrets.yaml`

**Validation commands:**
- Fast: `just nixOpts= eval-nixos`
- Comprehensive: `just eval-all`
- Lint: `just lint`

**Secrets commands (use nested path for hierarchical structure):**
```bash
sops set secrets/docker-on-nixos/secrets.yaml '["service"]["secret"]' "$(echo '"'$(apg -x16 -m16 -MLCN -n1)'"')"
```

**Module structure:**
1. Outer: `{ inputs, self, config, ... }:` with `let flakeConfig = config; in`
2. Module: `flake.nixosModules.name = { config, lib, ... }:`
3. Key: `key = "nixos-config.modules.nixos.name";`
4. Imports: All dependencies explicitly listed
5. Config: User, secrets, dirs, arion, tailscale

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.