Secrets and configs

Secrets

Setting one of uid/gid and not the other leaves the other at 0, as docker does. Setting neither is where we differ: the file is written owned by the instance user, where docker uses root. A 0400 secret owned by root is unreadable to the non-root user most OCI images run as, which makes docker's default useless here. The same applies to configs[].

Configs

configs:
  app_config:
    file: ./app_config.txt

services:
  app:
    configs:
      - app_config
      - source: app_config
        target: /etc/app/config.txt
        uid: "1000"
        gid: "1000"
        mode: 0o440

Overwriting Image Files

Configs and secrets are written into the instance before it first starts, and they replace a file the image already ships at that target. This is how you override an application's own default config:

services:
  web:
    image: docker.io/library/caddy:2-alpine
    configs:
      - source: caddyfile
        target: /etc/caddy/Caddyfile

configs:
  caddyfile:
    file: ./Caddyfile

Docker achieves the same by mounting over the path, so the image file is only hidden for the container's lifetime. incus-compose writes into the instance's root filesystem instead, so the replacement is permanent for that instance - the original is gone until the instance is recreated.

A target inside a volume is written into that volume instead, since a mount would otherwise hide it. So a config lands on top of what prefetching put there, which is the order docker mounts them in:

services:
  web:
    image: docker.io/nginx:alpine
    volumes:
      - conf:/etc/nginx/conf.d
    configs:
      - source: site
        target: /etc/nginx/conf.d/site.conf

The volume gets the image's default.conf and your site.conf beside it, and site.conf is rewritten on every start. A target under a tmpfs or a pass-through bind has nowhere to be written before the instance starts, so it is warned about and skipped.

Changed in 1.3.0: such a file used to be written into the instance's filesystem, where the mount hid it.

Changed in 1.2.0: a target that already existed in the image was previously left untouched, which silently ignored the config or secret.