Skip to content

docker-compose: Run services with a read-only root filesystem - #1470

Open
marcleblanc2 wants to merge 5 commits into
marc/prometheus-read-only-root-filesystemfrom
read-only-root-filesystem
Open

marcleblanc2 wants to merge 5 commits into
marc/prometheus-read-only-root-filesystemfrom
read-only-root-filesystem

Conversation

@marcleblanc2

@marcleblanc2 marcleblanc2 commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Some customers need a read-only root filesystem on every container

No Compose service set read_only: true, so a process could modify its own container image at runtime

  • Code scanning flagged each service with sg-infra-writable-filesystem-service

Solution

Set read_only: true on every service in every Compose file, except prometheus

Services that write outside their volumes get a writable path, mirroring the Helm chart's emptyDir mounts where it has them:

Service Writable path
gitserver, searcher, blobstore, executor tmpfs /tmp
frontend, frontend-internal tmpfs /tmp, new named volume at /home/sourcegraph
pgsql, codeintel-db, codeinsights-db new named volume at /var/run/postgresql
precise-code-intel-worker, syntactic-code-intel-worker new named volume at /tmp

Review notes:

  • /var/run/postgresql and /home/sourcegraph are named volumes, because a tmpfs over an existing directory is owned by root, which broke postgres in CI
    • A named volume takes the owner and mode of the image directory, so the Compose file names no UID, GID, or mode
  • The code intel workers' /tmp is on disk, because uploads can be several GB, and a tmpfs counts against mem_limit
  • caddy, cadvisor, blobstore, otel-collector, the postgres exporters, and the executor have no read-only precedent in Helm
    • They are read-only here from reading their entrypoints, and the executor has no CI coverage
  • Breaking for anyone who writes into a container by hand (docker exec, docker cp) outside a volume

Not changed:

Test plan

  • docker-compose config -q (v1 1.29.2, same as CI) passes for the main file, the db-only file, the executor file, and the jaeger and dev overlays
  • The Semgrep rule goes from 30 findings to 1 (prometheus)

Docker Compose

  • CI smoke test: starts the stack, expects 25 running containers, and curls the frontend
    • This proves the services start, not that every runtime path works

Not run:

  • Cloning, SCIP uploads, executor jobs, the jaeger overlay, and the db-only file

Helm

Linked PRs

Amp threads

@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

marcleblanc2 and others added 5 commits November 12, 2026 23:56
Set read_only: true on every service except prometheus, whose image writes
its alertmanager config and data outside the mounted volumes.

Writable paths follow the Helm chart's emptyDir mounts: /tmp for gitserver
and searcher, /home/sourcegraph for the frontends, /var/run/postgresql for
the databases. The frontends and blobstore also get /tmp. The two code
intel workers get /tmp as a named volume, because their scratch files can
be several GB and a tmpfs counts against mem_limit.

Amp-Thread-ID: https://ampcode.com/threads/T-01a13cfb-9756-7111-8b29-381a841065e1
Co-authored-by: Amp <amp@ampcode.com>
A tmpfs mounted over an existing directory keeps that directory's mode but
is owned by root, so postgres could not create its socket lock file in
/var/run/postgresql and the smoke test failed. Set uid and gid on the
/var/run/postgresql and /home/sourcegraph mounts to the image's user.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a13cfb-9756-7111-8b29-381a841065e1
The postgres user is uid 999 in the postgresql-16 image but uid 70 in the
codeinsights image, so codeinsights-db still could not write its socket
lock file. A world-writable sticky tmpfs, like /tmp, works for any user.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a13cfb-9756-7111-8b29-381a841065e1
The executor writes workspaces under TMPDIR, which is the /scratch bind
mount. The git, docker, and src commands it runs do not inherit TMPDIR, so
/tmp is a tmpfs.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a13cfb-9756-7111-8b29-381a841065e1
CI showed mode=1777 has no effect on a tmpfs mounted over an existing
image directory: postgres still could not write to /var/run/postgresql.

A named volume takes the owner and mode of the image directory it
covers, so it is writable by the image's user without naming a uid, gid,
or mode in the compose file. /tmp stays a tmpfs: it is 1777 in the images.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a1d36e-7d38-77c8-98dc-e4678a3ad2b0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant