Skip to content

Filesets

Filesets keep a local directory in sync with a path inside a Docker volume. They let you manage configuration files, static assets, or seed data declaratively, without baking files into images.

Filesets follow the same pattern as stacks: discover by convention, augment with explicit config.

  • Declarative sync Define the source and target; Dockform syncs diffs only.

  • Idempotent and incremental Only changed, added, or removed files are applied.

  • Auto-discovery Filesets are found in <context>/<stack>/volumes/ directories.

  • Optional service restarts Configure services to restart after files are updated.

Automatic Discovery

Dockform discovers filesets from volumes/ directories inside each stack:

my-project/
├── dockform.yml
├── default/
│   └── web/
│       ├── compose.yaml
│       └── volumes/           # ← Filesets directory
│           ├── config/        # ← Fileset "config"
│           │   └── nginx.conf
│           └── static/        # ← Fileset "static"
│               ├── index.html
│               └── styles.css

Each subdirectory of volumes/ becomes a fileset. The directory name is used as:

  • The fileset name
  • The target volume name (created if it doesn't exist)

Discovery Defaults

For each discovered fileset:

Property Default Value
source <stack>/volumes/<fileset>/
target_volume <fileset> (same as directory name)
target_path /<fileset> (root path with fileset name)
apply_mode hot

Customizing Discovery

Change the volumes directory name:

identifier: myapp

discovery:
  volumes_dir: data  # Look for <stack>/data/ instead of <stack>/volumes/

contexts:
  default: {}

Configuring Filesets in the Manifest

Use the filesets: block inside a stack definition to override discovered defaults or declare filesets explicitly. This follows the same discover-then-augment pattern used for stacks.

Overriding Discovered Filesets

When a fileset is discovered from volumes/, you can override any of its defaults:

identifier: myapp

contexts:
  default: {}

stacks:
  default/web:
    filesets:
      config:                                 # ← Must match the directory name
        target_volume: my-custom-volume       # ← Override default volume name
        target_path: /etc/nginx               # ← Override default target path
        ownership:
          user: "1000"
          group: "1000"
          file_mode: "0644"
          dir_mode: "0755"
        exclude:
          - "*.tmp"
          - .DS_Store

Explicit values override discovered defaults. Fields you don't specify keep their discovered values.

Declaring Filesets Explicitly

You can also define filesets entirely in the manifest without a volumes/ directory. This is useful when the source files live outside the stack directory or when you need full control over the configuration:

identifier: myapp

contexts:
  default: {}

stacks:
  default/web:
    filesets:
      config:
        source: ./shared/nginx-config    # ← Source outside the stack directory
        target_volume: nginx_config
        target_path: /etc/nginx
        exclude:
          - "*.bak"

Fileset Fields

Field Type Description
source string Local directory to sync from
target_volume string Docker volume to sync into
target_path string Path inside the volume (must be absolute, default: /)
apply_mode string hot (default) or cold
exclude list Gitignore-style patterns to exclude
ownership object Ownership and permission settings
restart_services string or list Services to restart after sync

Using Filesets with Compose

Reference fileset volumes as external in your compose files:

default/traefik/
├── compose.yaml
└── volumes/
    └── config/
        ├── traefik.yaml
        └── dynamic/
            └── routers.yaml
services:
  traefik:
    image: traefik:v3
    volumes:
      - config:/etc/traefik

volumes:
  config:
    external: true

The config volume is automatically created and synced with the contents of volumes/config/.

How Sync Works

Dockform builds a content index from the local source and compares it with a remote index inside the volume:

  1. Index storage: .dockform-index.json inside the target path
  2. On differences:
  3. Packs created/updated files in a tar archive
  4. Extracts them into the volume
  5. Deletes files present remotely but absent locally
  6. Writes the new index file
  7. No changes: If tree hashes match, the fileset is skipped

Apply Modes

Filesets support two apply modes that control how files are synchronized with running containers:

Hot Mode (Default)

With apply_mode: hot, Dockform syncs files while containers are running, then restarts configured services.

Hot mode workflow:

  1. Sync files to volume (containers keep running)
  2. Apply stack changes via docker compose up
  3. Restart services if configured

Cold Mode

With apply_mode: cold, Dockform stops services first, syncs files, then starts them again.

Use cold mode when:

  • Files must not change while services are running
  • Atomic updates across multiple files are required
  • Database or critical system configurations are being updated
stacks:
  default/db:
    filesets:
      init:
        apply_mode: cold

Ownership & Permissions

Filesets can enforce ownership and permission bits on synced files via the ownership block:

stacks:
  default/web:
    filesets:
      config:
        ownership:
          user: "1000"
          group: "1000"
          file_mode: "0644"
          dir_mode: "0755"
          preserve_existing: false
Field Description
user Numeric UID or POSIX username
group Numeric GID or POSIX group name
file_mode Octal permission for files (e.g., "0644")
dir_mode Octal permission for directories (e.g., "0755")
preserve_existing When true, only new/updated files get ownership applied

Tip

Use numeric IDs for portability. Named users/groups must exist inside the helper container (alpine:3.22).

Preserve Existing

When preserve_existing: true, ownership and permissions are only applied to files that are newly created or updated during the current sync. Files that already exist in the volume and haven't changed keep their current ownership.

This is useful when:

  • Multiple filesets or processes write to the same volume
  • You want to avoid overwriting manually set permissions
  • Initial deploy sets ownership, subsequent syncs only touch new files

Excluding Files

Use the exclude field with gitignore-style patterns:

stacks:
  default/web:
    filesets:
      static:
        exclude:
          - .git/
          - "**/.DS_Store"
          - "*.tmp"
          - node_modules/

Restarting Services

Configure services to restart after a fileset is synced using restart_services:

stacks:
  default/web:
    filesets:
      config:
        restart_services: attached    # ← Restart services that mount the target volume

restart_services accepts:

Value Behavior
attached Automatically discover and restart services that mount the fileset's target volume
List of strings Restart the named services explicitly
# Explicit service list
stacks:
  default/web:
    filesets:
      config:
        restart_services:
          - nginx
          - sidecar

Lifecycle and Operations

Step Operation
plan Shows file operations per fileset when Docker is available
apply Ensures volume exists, computes diffs, syncs changes, writes index, queues restarts
destroy Removes fileset-associated volumes along with other labeled resources

Multi-Context Filesets

Filesets are discovered per context, allowing different configurations for different environments:

my-project/
├── dockform.yml
├── default/
│   └── nginx/
│       └── volumes/
│           └── config/
│               └── nginx.dev.conf
└── production/
    └── nginx/
        └── volumes/
            └── config/
                └── nginx.prod.conf

Examples

Basic: Static Site Assets

default/web/
├── compose.yaml
└── volumes/
    └── html/
        ├── index.html
        ├── styles.css
        └── app.js
default/web/compose.yaml
services:
  nginx:
    image: nginx:alpine
    volumes:
      - html:/usr/share/nginx/html

volumes:
  html:
    external: true

Traefik with Custom Target Path

default/traefik/
├── compose.yaml
└── volumes/
    └── config/
        ├── traefik.yaml
        └── dynamic/
            └── routers.yaml
identifier: myapp

contexts:
  default: {}

stacks:
  default/traefik:
    filesets:
      config:
        target_path: /etc/traefik
        restart_services: attached
services:
  traefik:
    image: traefik:v3
    command:
      - --configFile=/etc/traefik/traefik.yaml
    volumes:
      - config:/etc/traefik

volumes:
  config:
    external: true

Database Seeds with Ownership

dockform.yaml
identifier: myapp

contexts:
  default: {}

stacks:
  default/db:
    filesets:
      init:
        target_path: /docker-entrypoint-initdb.d
        apply_mode: cold
        ownership:
          user: "999"
          group: "999"
          file_mode: "0640"
default/db/compose.yaml
services:
  postgres:
    image: postgres:16
    volumes:
      - init:/docker-entrypoint-initdb.d

volumes:
  init:
    external: true

Run dockform plan to preview fileset changes, then dockform apply to sync files and start services.