December 4, 2025·4 min read·engineeringrqlitedockersecuritystorage

rqlite 9.3.4 runs its Docker image as user 1000

The image now selects an unprivileged runtime user and prepares its data directory for that identity. Existing mounted storage remains part of the upgrade contract.

A database container's user determines whether its process can write the files it needs. rqlite 9.3.4, released on 4 December, changes that runtime identity to a dedicated non-root user. The changelog explicitly says there are no rqlite functional changes relative to 9.3.3. The operational difference belongs to the Docker image.

I value rqlite's small deployment footprint, but the image around the binary is still part of the system. A packaging-only release can change whether an existing installation starts successfully. Here, ownership of persistent storage is as relevant to the upgrade as the server version.

The image selects a specific identity

PR #2400 creates a group named rqlite with group ID 1000 and a user of the same name with user ID 1000. The Dockerfile then selects USER rqlite before its entrypoint and default command.

This choice has two parts. The account name makes the image definition readable. Numeric IDs determine the identity represented inside the container's filesystem model. An external directory cannot grant access merely because its owner has a similar account name on another system.

The released Dockerfile also creates /rqlite/file and changes its ownership to the new user and group during image construction. That prepares the directory built into the image for the selected runtime identity.

The runtime directory preparation occurs before USER rqlite. Installing packages and constructing the image's directories remain build operations. Switching the runtime user does not mean every step of image construction has to execute with that identity.

A compact extract shows the resulting image contract without reproducing the surrounding build stages.

RUN addgroup -g 1000 rqlite && \
    adduser -u 1000 -G rqlite -D rqlite
RUN mkdir -p /rqlite/file && \
    chown -R rqlite:rqlite /rqlite/file
USER rqlite

The selected account is part of the delivered image. The change does not add a server flag that applications have to pass on every invocation.

The entrypoint starts under the selected user

The Dockerfile retains docker-entrypoint.sh and the default run command. The released entrypoint constructs a server invocation from its environment and supplied arguments, then replaces itself with the chosen command through exec.

There is no added startup branch that begins as root, changes mounted ownership, and then drops privileges. The Dockerfile selects the non-root account before the entrypoint runs. That makes the image default explicit and avoids making startup ownership repair an implicit side effect.

The entrypoint's default data directory remains /rqlite/file/data. An environment override can select another directory, but the image build only prepares its known directory. Selecting a different path does not cause the script to create a corresponding privileged ownership adjustment.

This distinction affects deployments with custom data locations. The configured runtime identity must have the access needed at the chosen location. A successful image build only establishes that the image's own prepared directory has the intended ownership.

Mounted storage has its own permissions

Docker's bind-mount documentation explains that a mount hides the image's pre-existing contents at the destination. Consequently, changing ownership of a directory during image construction does not rewrite the ownership of a host directory later mounted over it.

That is the main compatibility consequence to assess for an existing installation. The new image can contain a correctly prepared directory while a bind-mounted data directory still has permissions appropriate to the old runtime identity. The mount and the image are separate inputs.

For a native Linux deployment without additional identity mapping, access must be consistent with the process's numeric user and group IDs. Other runtime arrangements can add mapping rules. The release fixes the image's default identity, while the deployment still determines which storage is mounted and how that identity reaches it.

The useful conclusion is specific. A mounted directory must permit the new runtime identity to perform the database's required file operations. The patch does not provide a universal command for changing arbitrary host ownership, and treating every persistent directory as safe to rewrite would be an unsuitable upgrade policy.

My preference is to manage storage access in the deployment definition. A database image should have a clear user, and the provisioned storage should support that user. This keeps an ownership decision attached to the resource that actually controls it.

Extension assets remain separate from writable state

The image copies its server and shell binaries into /bin, and its bundled extension archives into /opt/extensions/. The new recursive ownership change targets /rqlite/file. It does not make the binaries and bundled extensions belong to the runtime user as part of this patch.

That layout distinguishes shipped executable content from the directory intended for persistent database files. The Dockerfile continues to build the extension archives in its builder stage and copy them into the runtime stage. Changing the selected runtime account does not remove those assets.

Custom extensions or credential files mounted by a deployment still need readable paths under the chosen identity. This follows from moving the process identity, rather than from a new extension-loading algorithm. A permission failure at an external file should be investigated at that file's mount and access rules.

The port declaration also changes

The same diff corrects the image's exposed-port declaration from a repeated 4001 entry to 4001 and 4002. That adjustment is visible in the packaging change even though the release's main purpose is the runtime user. It does not represent a new database protocol.

The release therefore has a narrow application-code scope and a concrete deployment scope. According to the changelog, binary behavior remains that of the preceding patch. Docker startup now defaults to user and group 1000, with the built-in data directory prepared for that identity and external storage permissions supplied by the deployment.

This also separates updating a downloaded server binary from updating the official container image. Only the latter introduces the Dockerfile's runtime-user selection. An installation that supplies its own process user outside Docker needs to assess its own service configuration independently.