Preserve Local Configuration Changes with ucf

ucf installs a maintainer's configuration file without clobbering the local edits an admin made to the live one. By the end of this guide, you will also be able to preview the decision and remove ucf's recorded state when a package is purged. The examples use harmless files under /tmp; adapt the paths only after you have checked them.

Allow about 15 minutes. You need a Debian-family Linux system with the ucf package installed, a shell, and permission to read the source file and write the destination. Updating a real file under /etc normally requires sudo. This guide was checked with Debian package version 3.0043+nmu1; option names and the state-file behaviour below come from the installed ucf(1) and ucf.conf(5) pages.

1. Check the installed command

ucf is primarily a package-maintainer tool. Its two positional arguments are not "old file" and "new file": the first is the new file supplied by a package, and the second is the live destination that may contain user edits. It keeps a hash record in /var/lib/ucf/hashfile by default and can ask through debconf when it cannot safely infer the right choice.

$ dpkg-query -W -f='${Package} ${Version}\n' ucf
ucf 3.0043+nmu1
$ command -v ucf
/usr/bin/ucf

If the package is missing, stop here and install it through your normal package-management process. Do not copy a random script named ucf into your path: this command is meant to cooperate with Debian package state.

Checkpoint: You have confirmed both the package version and the executable that will run.

2. Build a disposable source and destination

Start with two different files so the decision is visible. The source represents the version shipped or generated by a package. The destination represents the file an administrator may have edited. These commands change only /tmp.

$ work=/tmp/ucf-example
$ mkdir -p "$work/state"
$ printf '%s\n' 'setting=maintainer-default' > "$work/new.conf"
$ printf '%s\n' 'setting=local-value' > "$work/destination.conf"
$ diff -u "$work/new.conf" "$work/destination.conf" || true
--- /tmp/ucf-example/new.conf
+++ /tmp/ucf-example/destination.conf
@@
-setting=maintainer-default
+setting=local-value

The || true is deliberate: diff returns a non-zero status when files differ, but that difference is the expected checkpoint rather than a command failure.

3. Preview an update before changing the destination

Use --no-action for a dry run and --state-dir to keep the example's hash and cache outside the system directory. The latter is documented mainly for testing, which makes it useful here.

$ ucf --no-action --state-dir "$work/state" \
    "$work/new.conf" "$work/destination.conf"
mkdir -p /tmp/ucf-example/state/cache

When the files differ, an interactive run may need debconf to present the choice. A dry run is still the right first check, but its exact diagnostic depends on whether a decision can be made without prompting. If you need a deterministic non-interactive test, set one of the force variables shown in the next step and retain --no-action. Do not treat a dry run as proof that the destination has changed.

$ grep -Fx 'setting=local-value' "$work/destination.conf"
setting=local-value
$ test ! -e "$work/state/hashfile" && echo 'no state file created'

Checkpoint: The destination still contains the local value. Nothing in this step is allowed to overwrite it.

4. Choose the update policy explicitly

By default, ucf asks when it cannot determine whether the local file should be replaced. The environment variables UCF_FORCE_CONFFOLD and UCF_FORCE_CONFFNEW silently retain the installed file or replace it with the new file. Set only one. For a package script, a policy should be chosen deliberately rather than inherited accidentally from a user's environment.

To preview a replacement without changing the file, use the new-file policy with the disposable state directory:

$ UCF_FORCE_CONFFNEW=YES ucf --no-action \
    --state-dir "$work/state" "$work/new.conf" "$work/destination.conf"
Replacing config file /tmp/ucf-example/destination.conf with new version
since you asked for it.

For an actual replacement, remove --no-action. This is a destructive configuration change: make a backup first and confirm the destination path is exact.

$ cp --preserve=all "$work/destination.conf" "$work/destination.conf.before-ucf"
$ UCF_FORCE_CONFFNEW=YES ucf --state-dir "$work/state" \
    "$work/new.conf" "$work/destination.conf"
$ cmp -s "$work/new.conf" "$work/destination.conf" && echo 'destination now matches new.conf'
destination now matches new.conf

If you chose the wrong policy, restore the backup and remove the disposable state directory before trying again. For a real file, restore through your normal configuration backup process; ucf's --purge option does not restore or delete the destination.

5. Understand the state and the upgrade path

After a real update, ucf records the source checksum in its hash file and may keep copies with suffixes such as ucf-old, ucf-new, or ucf-dist. These records let a later package upgrade distinguish an unchanged maintainer version from a locally edited file. A package should also register the destination with ucfr so that the association can be queried with ucfq.

For a package that has a known previous maintainer version, historical checksums can sit beside the new source in new.conf.md5sum or in the new.conf.md5sum.d/ directory. A file named default has special meaning when no listed checksum matches. This is package-authoring data, not a file to invent during an emergency upgrade: a wrong checksum can make a locally changed file look untouched.

The optional --three-way mode lets ucf offer a merge of the old maintainer version, the new maintainer version, and the local destination. It uses diff3 and may still need a human decision. Review the proposed result before accepting it; a merge can be syntactically valid and still be wrong for the service.

6. Set site-wide defaults only with a controlled change

/etc/ucf.conf is sourced as a Bourne shell snippet, not parsed as a passive key-value file. It can set DEBUG, VERBOSE, conf_force_conffold, conf_force_conffnew, conf_source_dir, and conf_old_mdsum_file. The precedence is defaults, then this file, then environment variables, then command-line options.

Do not enable both force policies. If you need a system-wide default, edit the file with root privileges, keep a backup, and make the smallest possible change. For example, retaining local files can be set as follows:

# /etc/ucf.conf
conf_force_conffold=YES

That policy affects future ucf invocations on the machine. Prefer a per-run environment variable or command-line option when only one package operation needs it. Check the effective behaviour with a disposable source, destination, and --state-dir before applying it to /etc.

7. Purge recorded state when the package is removed

A package's post-removal script should tell ucf to forget the destination when the package is purged. The command removes the file's entries from ucf's state, but it does not remove the destination on disk. File removal remains the package's responsibility.

$ ucf --purge --state-dir "$work/state" "$work/destination.conf"
$ test ! -e "$work/state/hashfile" || ! grep -F "$work/destination.conf" "$work/state/hashfile"
$ echo 'ucf state no longer tracks destination.conf'
ucf state no longer tracks destination.conf

For a real package-maintainer script, use ucf --purge /etc/example.conf while ucf is still available, and separately remove the package-owned file if that is the intended purge behaviour. Do not run this against an active service's configuration merely to force a new prompt: it discards ucf's recorded history and can change the next upgrade decision.

Done means