| name | nix-oci-image |
| description | Building and publishing OCI/Docker container images from Nix — dockerTools.buildLayeredImage over a buildEnv (app + cacert), non-root numeric User for Kubernetes runAsNonRoot, version-derived tags, and pushing with skopeo (docker-archive → OCI) to registries like Zot that reject `docker push`. Apply when adding a container image output to a flake; when writing packages/docker/*.nix or dockerTools code; when a Nix-built image fails TLS (missing CA bundle), lacks PATH, or runs as root; when pushing ./result to a private/homelab registry fails; when wiring an image build+push recipe into a justfile or CI. Do NOT use for Dockerfile-based builds, docker-compose, or Kubernetes manifest authoring. |
| user-invocable | true |
| license | MIT |
| compatibility | Designed for Claude Code, Codex or similar harness, and for Nix flakes producing container images with nixpkgs dockerTools. |
| metadata | {"author":"aldoborrero","version":"1.0.0","openclaw":{"emoji":"📦","homepage":"https://github.com/aldoborrero/cc-skills-nix","requires":{"bins":"[Truncated]"},"install":[]}} |
| allowed-tools | Read Edit Write Glob Grep Bash(nix:*) Bash(git:*) Bash(skopeo:*) Bash(just:*) Agent |
| paths | ["**/docker/*.nix","**/packages/docker/**"] |
OCI images from Nix (dockerTools + skopeo)
Reference implementation: aldoborrero/reolink-exporter (nix/packages/docker/). The image is just another blueprint package: nix build .#docker produces a docker-archive tarball at ./result; skopeo publishes it. No Dockerfile, no daemon at build time.
The package (two-file pattern, image depends on the app)
# nix/packages/docker/default.nix — blueprint shim
{ pkgs, perSystem }:
pkgs.callPackage ./package.nix { myapp = perSystem.self.myapp; }
# nix/packages/docker/package.nix
{
dockerTools,
buildEnv,
myapp,
cacert,
}:
let
env = buildEnv {
name = "myapp-env";
paths = [
myapp
cacert # TLS trust root — without it every HTTPS call fails
];
pathsToLink = [ "/bin" "/etc" "/lib" "/share" ];
};
in
# `docker run -e MYAPP_OPT=... -p 8000:8000 myapp:0.2.0`
dockerTools.buildLayeredImage {
name = "myapp";
tag = myapp.version; # tag follows the package version, never "latest"
contents = [ env ];
config = {
Entrypoint = [ "/bin/myapp" ];
# Numeric so Kubernetes `runAsNonRoot` is satisfied without an /etc/passwd entry.
User = "10001:10001";
Env = [
"PATH=/bin"
"SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"
"PYTHONUNBUFFERED=1" # language-appropriate runtime flags
];
ExposedPorts."8000/tcp" = { };
};
}
Why each piece:
buildEnv: merges app + cacert into one store path with a predictable /bin, /etc/ssl layout; pathsToLink controls what gets linked.
cacert + SSL_CERT_FILE: there is no distro base layer, so no CA bundle exists unless you add one — missing it surfaces only at runtime, as certificate errors.
buildLayeredImage: store-path-per-layer, so app-only changes reuse dependency layers on push/pull. Output: a compressed docker-archive tarball at ./result.
PATH=/bin: no shell profile sets it for you.
The image inherits blueprint's package plumbing: appears as packages.<sys>.docker and builds under checks.<sys>.pkgs-docker in nix flake check.
Publishing: skopeo, not docker
# justfile
registry := env_var_or_default("REGISTRY", "registry.example.com")
image := "myapp"
image:
nix build .#docker --log-format bar-with-logs
# Push the built image. Zot rejects `docker push`, it needs OCI via skopeo.
push tag: image
skopeo copy \
docker-archive:./result \
docker://{{ registry }}/{{ image }}:{{ tag }} \
--format oci