LPIC-1 101-500 study materials
LPIC-1 101-500 study resources

Packaging acme-report under the FHS on a read-only root

LPIC-1 101-500 · Put your knowledge to work in context.

All case studies

Packaging acme-report under the FHS on a read-only root

An in-house Java application, acme-report, ships as a vendor tarball. Its launcher, libraries and a bundled JRE live under /opt/acme/report/; it reads /opt/acme/report/conf/report.properties, writes rotating logs to /opt/acme/report/logs/, and keeps an on-disk index in /opt/acme/report/data/ that must survive application upgrades. It runs as user acme, which has no privileges. The 60 target hosts run Debian 12 with the root filesystem mounted read-only in normal operation; /etc, /var, /home and /srv are separate writable filesystems, and /usr is part of the read-only root. The packaging team can create directories and symlinks when building the golden image but will not patch the vendor's source. Compliance requires configuration files under /etc so they are captured by the existing /etc backup job.

1. Place the configuration, logs and index according to the FHS and justify each, including why /etc/opt and /var/opt exist at all.

Read the worked answer

The FHS treats /opt/<package> as a self-contained add-on package and splits its static content from its variable content:

  • Program, libraries, bundled JRE stay in /opt/acme/report/bin, lib, jre. That is what /opt is for: software not installed by the distribution's package manager, self-contained so it can be removed wholesale.
  • Configuration goes to /etc/opt/acme/report/report.properties. /etc/opt exists precisely so that host-specific configuration for /opt packages lives under /etc, where the FHS says configuration belongs - here that also satisfies compliance by putting the file in the existing /etc backup job.
  • Logs go to /var/log/acme-report/. The FHS states that system log files belong under /var/log, including for /opt packages; /var/opt is the fallback for a package's other variable data, not for logs.
  • Index / on-disk state goes to /var/opt/acme/report/data/, which the FHS defines as variable data of the package - data that survives the vendor tarball being replaced.

All three targets are on writable filesystems, so a read-only root is no obstacle. /srv is the wrong home: it is for data served out by this system to others, not for a local application's state. Keeping conf and data under /opt would put mutable state on the read-only root and, for the index, lose it at the next upgrade of the tarball.

2. The code is fixed at those /opt paths. Show how the image build makes it work, and rank symlinks against bind mounts.

Read the worked answer

Create the real directories on the writable filesystems and the symlinks in the image:

/opt/acme/report/conf  -> /etc/opt/acme/report
/opt/acme/report/logs  -> /var/log/acme-report
/opt/acme/report/data  -> /var/opt/acme/report/data

with chown -R acme:acme on the targets (ownership of a symlink is irrelevant; the kernel checks the target). Because the launcher opens those paths normally, the kernel resolves the links and the code never notices.

Why symlinks are the better option here:

  • A symlink is static metadata, so it can live on the read-only root and is part of the image - nothing has to run at boot and nothing needs root.
  • Failure is immediate and loud: a missing target gives ENOENT at open(), which is visible in the app log and in ls -l /opt/acme/report.
  • They cost nothing and do not complicate du, df or backup exclusion lists.

Bind mounts (fstab entries, or systemd units with RequiresMountsFor=) also work, but they add boot ordering, need root, and - the real trap - if the bind fails the mount point silently remains the underlying directory. On a read-only /opt that fails closed; on any host where /opt is writable, the app then writes its index into the image and the data is lost at the next refresh.

One image-build follow-up: upgrading the vendor tarball must not overwrite the three symlinks. Add a post-install check (test -L on each) and put the logrotate rule in /etc/logrotate.d/acme-report, since logrotate reads its configs from /etc, not from /opt.

3. A colleague's script installs a compiled helper into /usr/local/bin and writes a pidfile to /usr/local/var/run/acme.pid; it fails at start. Explain both problems and give the FHS-correct alternatives.

Read the worked answer

Two different rules are being broken.

  1. /usr/local is static data, not working space. The FHS defines /usr/local as a hierarchy that mirrors /usr (bin, sbin, lib, share, ...) for software installed by the local administrator outside the package manager. Its contents are program and library files, not runtime state. On these hosts /usr is part of the read-only root, so the mkdir and the pidfile write both fail; even on a writable host, runtime state under /usr/local breaks the read-only/variable-data split the image is built around.
  2. /var/run is not a normal directory. On Debian 12 it is a symlink to /run, a tmpfs recreated empty at every boot; and /bin, /sbin and /lib are merged into /usr (usrmerge). A pidfile in a tmpfs path is only accidentally correct and is lost on reboot, and systemd expects runtime files under /run/<service>.

Correct placements:

  • The compiled binary itself may stay in /usr/local/bin - that is its proper home for non-distribution software and it precedes /usr/bin in the default PATH - but it must be baked into the image, not written at runtime.
  • The pidfile belongs in /run/acme.pid, ideally created by a systemd unit using RuntimeDirectory=acme, which handles directory creation, ownership and cleanup on stop.
  • Genuinely persistent local state goes to /var/local, the FHS-sanctioned place for variable data owned by software installed in /usr/local.