Skip to content

chore(infra): derive DAO API database memory settings from cgroup limit - #2099

Draft
pikonha wants to merge 3 commits into
devfrom
chore/dao-api-db-image
Draft

chore(infra): derive DAO API database memory settings from cgroup limit#2099
pikonha wants to merge 3 commits into
devfrom
chore/dao-api-db-image

Conversation

@pikonha

@pikonha pikonha commented Aug 1, 2026

Copy link
Copy Markdown
Member

Why

All DAO databases run the stock postgres-ssl image with zero tuning, and every one sampled carries a hand-applied max_connections = 500 in postgresql.auto.conf — undocumented, invisible outside the volume, applied across four different memory tiers.

service memory.max auto.conf
uniswap-db 8 GB max_connections = 500
aave-db 6 GB max_connections = 500
ens-db 4 GB max_connections = 500
compound-db 4 GB max_connections = 500
shutter-db 2 GB max_connections = 500
nouns-db 2 GB max_connections = 500

500 backends is ~2.5 GB of unreclaimable memory. On the 2 GB tier that is an OOM the container cannot survive. PostgreSQLHighConnections cannot catch it either — it fires at 80% sustained for: 10m, which the OOM beats.

What

Wrap the stock image. The entrypoint reads /sys/fs/cgroup/memory.max and appends derived values as -c flags.

cap max_connections effective_cache_size
2 GB 75 1.3 GB
4 GB 80 2.6 GB
6 GB 120 3.9 GB
8 GB 160 5.2 GB

Derived rather than hardcoded because the fleet spans four tiers off one image, and a Railway plan change moves a service between tiers without touching the repo.

-c flags outrank postgresql.auto.conf, so this neutralises the existing 500 without writing to any volume, and reverts by pointing a service back at the stock image.

shared_buffers deliberately stays at its 128 MB default — raising it converts reclaimable page cache into unreclaimable shmem, spending safety margin on the two tiers with the least of it. Separate change.

⚠️ Each DB service must set PG_MAJOR

Production runs 17 Postgres services across three majors: 16 (×8), 17 (×4), 18 (×5). The base image version comes from a PG_MAJOR build arg, which Railway populates from the service's variables.

There is no default on purpose. An unset PG_MAJOR fails the build, leaving the running container serving. A wrong default would build cleanly and then crashloop on database files are incompatible with server.

major services
16 uniswap, ens, compound, nouns, shutter, gitcoin, obol, telegram-bot, scroll
17 aave, address-enrichment, erpc, db-scroll
18 authful, fluid, lil-nouns, tornado, user

Verified locally

  • --self-test passes 8 cases: 4 fleet tiers, both clamp boundaries, 2 fallback paths
  • Builds and boots on all three majors with Railway's PGDATA; each reports max_connections=75, effective_cache_size=1331MB, ssl=on at a 2 GB limit
  • Unset PG_MAJOR fails the build, as designed
  • Derives correctly at 2 GiB (75) and 6 GiB (120)
  • ssl = on — the entrypoint wrap does not break the base image's cert setup
  • With max_connections = 500 written into a real auto.conf and restarted: SHOW max_connections75, sourcecommand line
bash infra/dao-api-db/entrypoint.dao-api-db.sh --self-test

Not in this PR

  • shared_buffers at 128 MB fleet-wide
  • Collation version mismatch (2.36 → 2.41) on uniswap-db — possible silent text-index corruption, needs REINDEX DATABASE before REFRESH COLLATION VERSION
  • postgres-exporter's DATA_SOURCE_NAME holds plaintext superuser credentials for 14 production databases in one variable

🤖 Generated with Claude Code

All 14 DAO databases run the stock postgres-ssl image with no tuning, and
every one of them carries a hand-applied `max_connections = 500` in
postgresql.auto.conf — undocumented, invisible outside the volume, and
applied across four different memory tiers (2/4/6/8 GB).

500 backends is roughly 2.5 GB of unreclaimable memory. On the 2 GB tier
(nouns-db, shutter-db) that is an OOM the container cannot survive, and
PostgreSQLHighConnections cannot catch it: it fires at 80% sustained for
10m, which the OOM beats.

Wrap the stock image so the entrypoint reads /sys/fs/cgroup/memory.max and
appends the derived values as -c flags. Command-line options outrank
postgresql.auto.conf, so this neutralises the existing 500 without writing
to any volume, and reverts by pointing a service back at the stock image.

  2 GB -> max_connections=75   effective_cache_size=1.3 GB
  4 GB -> max_connections=80   effective_cache_size=2.6 GB
  6 GB -> max_connections=120  effective_cache_size=3.9 GB
  8 GB -> max_connections=160  effective_cache_size=5.2 GB

shared_buffers is deliberately left at its 128 MB default: raising it
converts reclaimable page cache into unreclaimable shmem, which would spend
safety margin on the two tiers that have the least of it. Separate change.

Verify with: bash infra/dao-api-db/entrypoint.dao-api-db.sh --self-test

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
anticapture-storybook Ready Ready Preview Aug 5, 2026 5:28pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
anticapture Ignored Ignored Aug 5, 2026 5:28pm

Request Review

@railway-app

railway-app Bot commented Aug 1, 2026

Copy link
Copy Markdown

🚅 Deployed to the anticapture-pr-2099 environment in anticapture-infra

Service Status Web Updated (UTC)
fluid-api ✅ Success (View Logs) Aug 5, 2026 at 5:33 pm
aave-api ✅ Success (View Logs) Aug 5, 2026 at 5:33 pm
grafana ✅ Success (View Logs) Web Aug 5, 2026 at 5:33 pm
ens-relayer ✅ Success (View Logs) Aug 5, 2026 at 5:33 pm
lil-nouns-api ✅ Success (View Logs) Aug 5, 2026 at 5:33 pm
authful ✅ Success (View Logs) Web Aug 5, 2026 at 5:32 pm
shutter-api ✅ Success (View Logs) Aug 5, 2026 at 5:32 pm
ens-api ✅ Success (View Logs) Aug 5, 2026 at 5:32 pm
uniswap-api ✅ Success (View Logs) Aug 5, 2026 at 5:32 pm
gitcoin-api ✅ Success (View Logs) Aug 5, 2026 at 5:31 pm
tornado-api ✅ Success (View Logs) Aug 5, 2026 at 5:31 pm
gateful ✅ Success (View Logs) Web Aug 5, 2026 at 5:31 pm
scroll-api ✅ Success (View Logs) Aug 5, 2026 at 5:31 pm
compound-api ✅ Success (View Logs) Aug 5, 2026 at 5:30 pm
address-enrichment ✅ Success (View Logs) Web Aug 5, 2026 at 5:30 pm
prometheus ✅ Success (View Logs) Aug 5, 2026 at 5:29 pm
docs ✅ Success (View Logs) Web Aug 5, 2026 at 5:29 pm
nouns-api ✅ Success (View Logs) Aug 5, 2026 at 5:29 pm
loki ✅ Success (View Logs) Aug 5, 2026 at 5:28 pm
otelcol ✅ Success (View Logs) Aug 5, 2026 at 5:28 pm
obol-api ✅ Success (View Logs) Aug 5, 2026 at 5:28 pm
alertmanager ✅ Success (View Logs) Web Aug 5, 2026 at 5:28 pm
mcp ✅ Success (View Logs) Web Aug 5, 2026 at 5:27 pm
tempo ✅ Success (View Logs) Aug 5, 2026 at 5:27 pm
compound-indexer-offchain ✅ Success (View Logs) Aug 3, 2026 at 10:43 pm
gitcoin-indexer-offchain ✅ Success (View Logs) Aug 3, 2026 at 10:42 pm
ens-indexer-offchain ✅ Success (View Logs) Aug 3, 2026 at 10:39 pm
uniswap-indexer-offchain ✅ Success (View Logs) Aug 3, 2026 at 10:39 pm
shutter-indexer-offchain ✅ Success (View Logs) Aug 2, 2026 at 8:10 am
scroll-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
gitcoin-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
obol-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
ens-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
tornado-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
uniswap-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
compound-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
lil-nouns-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
shutter-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
aave-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
nouns-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
fluid-indexer ✅ Success (View Logs) Aug 1, 2026 at 4:10 pm
erpc ✅ Success (View Logs) Web Aug 1, 2026 at 4:10 pm
nodeful ✅ Success (View Logs) Aug 1, 2026 at 4:09 pm
user-api ✅ Success (View Logs) Web Aug 1, 2026 at 4:07 pm

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 074b1e2c74

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# The base image's ENTRYPOINT (wrapper.sh) sets up SSL certs and pgbackrest, so
# we don't replace it — entrypoint.dao-api-db.sh derives a couple of settings from
# the container's memory limit and then execs wrapper.sh with them appended.
COPY entrypoint.dao-api-db.sh /entrypoint.sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Copy the entrypoint from its repository-relative path

With the repository-root build context selected by dao-api-db.railway.toml, this source resolves to /entrypoint.dao-api-db.sh, but a repo-wide search shows the file exists only under infra/dao-api-db/; the image build therefore fails before deployment. Docker's COPY documentation specifies that local sources are resolved relative to the root of the build context, so this must copy infra/dao-api-db/entrypoint.dao-api-db.sh (or explicitly change the build context).

Useful? React with 👍 / 👎.

The fleet is not on one major: production runs 17 Postgres services across
16 (x8), 17 (x4) and 18 (x5). A hardcoded :16 base would start against a
newer PGDATA and exit with "database files are incompatible with server",
crashlooping 9 of 17 databases.

Take the major from a PG_MAJOR build arg, which Railway populates from the
service's variables. No default on purpose: an unset PG_MAJOR fails the
build and leaves the running container serving, whereas a wrong default
would build cleanly and then crashloop.

Booted and verified on all three majors at a 2 GB limit — each reports
max_connections=75, effective_cache_size=1331MB, ssl=on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to anticapture-infra / anticapture-pr-2099 August 1, 2026 16:12 Destroyed
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

🔍 Vercel preview: https://anticapture-ii0rl6kms-ful.vercel.app

@brunod-e

brunod-e commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Reviewed this while scoping the Q3 infra-cost KR. The core mechanism is sound — -c flags outranking postgresql.auto.conf neutralises the hand-applied 500 without touching a volume, --self-test passes all 8 cases locally, and it merges clean on dev. Four things to resolve before this leaves draft.

1. No watchPatterns — every push would restart all 17 databases

dao-api-db.railway.toml declares [build] with a dockerfilePath but no watchPatterns, and no [environments.pr] block. Every other repo-backed service that declares a build sets both: api, indexer, offchain-indexer, address-enrichment, docs, erpc, proxy, relayer — and postgres-exporter.railway.toml carries the pr NOOP override.

Once the DB services are pointed at this config, a merge to dev rebuilds and restarts the whole Postgres fleet. For stateless services that is cheap; for 17 production databases it is not. Worth pinning explicitly in the file rather than relying on the UI.

2. The "no healthcheck" justification depends on a service that isn't deployed

The config skips healthcheckPath on the grounds that:

Liveness is covered by postgres-exporter's pg_up, which feeds the PostgreSQLUnavailable alert in infra/monitoring/alerts.yml.

postgres-exporter has a railway.toml and a Dockerfile on dev (from #2082), but it does not appear among the 44 repo-connected Railway services in this PR's own check rollup — the deployed set is:

aave-api aave-indexer address-enrichment alertmanager authful compound-api
compound-indexer compound-indexer-offchain docs ens-api ens-indexer
ens-indexer-offchain ens-relayer erpc fluid-api fluid-indexer gateful gitcoin-api
gitcoin-indexer gitcoin-indexer-offchain grafana lil-nouns-api lil-nouns-indexer
loki mcp nodeful nouns-api nouns-indexer obol-api obol-indexer otelcol prometheus
scroll-api scroll-indexer shutter-api shutter-indexer shutter-indexer-offchain
tempo tornado-api tornado-indexer uniswap-api uniswap-indexer
uniswap-indexer-offchain user-api

If the exporter isn't running, the 17 databases end up with no HTTP healthcheck and no pg_up alerting. Either deploy the exporter first and make it a stated prerequisite, or don't lean on it in the rationale.

3. The max_connections floor counts Ponder but not the API

The floor is justified as:

~20 backends per GB, floored so Ponder's two default-30 pools always fit

That accounts for 60 of the demand. The API connects to the same database and opens three independent pools — drizzle(env.DATABASE_URL, …) at apps/api/cmd/index.ts:183, :188, and :400 — each getting node-postgres' default max: 10. The third (pgUnifiedClient) only instantiates when daoClient.supportOffchainData(), so it's 20 or 30 depending on the DAO.

Ceiling demand per DB is therefore closer to 60 (Ponder) + 30 (API) + exporter + pgbackrest + superuser_reserved_connections, ≈ 95:

service tier new cap ceiling demand
shutter-db 2 GB 75 ~90 (offchain → 3 API pools)
ens-db 4 GB 80 ~90 (offchain → 3 API pools)
compound-db 4 GB 80 ~90
nouns-db 2 GB 75 ~80

Pools are lazy and idle connections get reaped at node-postgres' default 10s, so this may well never bind in practice. But shutter and ens have the least headroom and would fail as FATAL: sorry, too many clients already under a burst, which is a worse failure shape than the slow degradation the floor was chosen to avoid. Confirming against real pg_stat_activity counts before rollout would settle it.

4. Merging this does not change anything by itself

None of the *-db services are in the list above — the DAO databases are Railway-managed and not repo-connected, which is also why no DB deploy check appears on this PR. Each of the 17 needs to be repointed at infra/dao-api-db/Dockerfile.dao-api-db with PG_MAJOR set, individually, in the UI. Worth stating in the PR body as an explicit rollout step so it doesn't read as merge-and-done.

Related, for sizing: since shared_buffers deliberately stays at 128 MB, the entire memory effect comes from shedding backends. At the ~5 MB/backend implied by this PR's own "500 backends is roughly 2.5 GB":

saved ≈ (actual_connections − new_cap) × 5 MB × 17 databases

Railway currently bills memory at roughly $10/GB/month, and the memory line is 74% of the bill (~90 GB resident against 1.42 vCPU average — the signature of idle backends holding RAM without consuming CPU). So if the fleet really is sitting near 500, this is worth a few hundred a month; if it's near 90, it's worth about $13. That difference is exactly what pg_stat_activity per database would tell us, which loops back to getting the exporter deployed first.

None of this argues against the change — 500 across a 2 GB tier is an OOM waiting to happen and the direction is right. Mostly it's the ordering: measure, deploy the exporter, then repoint.

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.

2 participants