Skip to content

Add APACHE_MAX_REQUEST_WORKERS to simplerisk-minimal - #178

Merged
jsokol merged 2 commits into
testingfrom
FEATURE-apache-max-request-workers
Oct 27, 2026
Merged

jsokol merged 2 commits into
testingfrom
FEATURE-apache-max-request-workers

Conversation

@jsokol

@jsokol jsokol commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Description

Adds an optional APACHE_MAX_REQUEST_WORKERS environment variable to simplerisk-minimal. The entrypoint validates it (whole number, 1-2000) and writes it to /etc/apache2/conf-enabled/simplerisk-mpm.conf, raising ServerLimit too when the value is above 256 (Apache silently lowers MaxRequestWorkers to ServerLimit otherwise). Unset keeps Apache's default of 150; an invalid value is ignored with a warning; unsetting it on a restarted container removes the file again.

Why: every SimpleRisk request holds a database connection. On 2026-10-26/27 two hosted instances on db.t3.micro databases (max_connections = 60) ran out of connections (SQLSTATE[HY000] [1040] Too many connections), and Apache's 150-worker default makes that easy to hit. This image runs in many environments (EKS with 2 replicas per tenant against a shared database, dedicated ECS tasks with db.t4g.micro defaults, self-hosted), so there is no single right number; the operator sets it per deployment. A rule of thumb is in the README: replicas x APACHE_MAX_REQUEST_WORKERS under the database's max_connections, with room for cron and the second connection used for DB-backed sessions.

Companion to code-development #2551, which adds a connection retry and an optional per-host cap in the application itself. Only the minimal image changes here; the full-stack image runs its own local MySQL.

What changed

  • simplerisk-minimal/common/entrypoint.sh: new set_apache_workers, called on every start before cron setup (independent of the database variables).
  • simplerisk-minimal/README.md and CLAUDE.md: document the variable.
  • simplerisk-minimal/test_set_apache_workers.sh plus a step in container-validation.yml: 48 checks (boundaries 1/256/257/2000, the exact generated file, leading zeros, removal on unset, rejection of non-numeric, zero, over-limit, decimal, negative, whitespace, shell-metacharacter, unicode and 64-bit-overflow values, the unwritable-path case, and that the entrypoint calls the function before exec).

Testing

  • test_set_apache_workers.sh: all checks pass; ShellCheck (-S error) clean on both scripts.
  • Against the official php:8.3-apache image: the generated file is included after mods-enabled/mpm_prefork.conf, apachectl -t reports Syntax OK, and starting Apache with 300 workers and no ServerLimit logs AH00180/AH00181 ... decreasing MaxRequestWorkers to 256, while the file this change writes (with ServerLimit 300) starts with no warning.
  • Built and run from this branch (docker build --build-arg php_version=8.3, ~3.5 min, builds cleanly). Verified in running containers:
    • APACHE_MAX_REQUEST_WORKERS=40 together with DB_SETUP=automatic against a real MySQL 8.0: the entrypoint logs Apache will serve at most 40 requests at once., writes /etc/apache2/conf-enabled/simplerisk-mpm.conf, the database is set up and the app answers (HTTP 302 from the login redirect).
    • APACHE_MAX_REQUEST_WORKERS=abc: logs the warning, writes no file, Apache starts with its default.
    • The cap is enforced. 40 parallel requests to a script that sleeps 6 s, sent from a sibling container: with APACHE_MAX_REQUEST_WORKERS=8 they took 32.0 s (five waves of 8), peaked at 9 Apache processes (parent + 8 children), all returned 200, and Apache logged server reached MaxRequestWorkers. With the variable unset they took 11.8 s, peaked at 69 processes, and logged no saturation.
  • Not run locally: the full container-validation matrix (all five variants, Dockle, Grype); it runs on this PR. Only the php 8.3 variant was built here.
  • Not covered: restarting a container with the variable removed (the entrypoint deletes the earlier file; checked by test_set_apache_workers.sh, not in a running container). Note that restarting a container started with DB_SETUP=automatic re-runs the database setup and exits, which is existing behaviour unrelated to this change.

Review

Reviewed for security and for correctness/tests (two reviewers, read-only, running the function with adversarial input). No must-fix findings. Two real bugs were found and fixed in the second commit:

  • Overflow: bash arithmetic wraps at 64 bits, so APACHE_MAX_REQUEST_WORKERS=18446744073709553616 evaluated to 2000 and was accepted. Leading zeros are now stripped and the length bounded before any arithmetic.
  • False success log: when the config file could not be written (for example a read-only root filesystem) the entrypoint still logged Apache will serve at most N requests at once, because a failed redirect on a { } group does not report failure. The file is now written with a single printf; a failure logs an error and keeps Apache's default.

Both are now covered by the test script, and the old code fails those new checks. After the fix the image was rebuilt and re-run: the cap still holds (40 parallel 6 s requests with a cap of 8: 31 s, peak 9 Apache processes), the overflow value is rejected with a warning, and an unwritable path logs an error while the container still starts.

Left as is: the APACHE_MPM_CONF_PATH test override is honoured in production (the operator already controls the container environment and the process is not root), and 2000 is a sanity bound rather than a memory-safe one, which the README now says.

Targets testing per the usual flow. It does not touch the Dockerfile, so it does not trigger promote-latest.

🤖 Generated with Claude Code

jsokol and others added 2 commits October 27, 2026 12:37
…che workers

Every request holds a database connection, and Apache's default of 150 workers
can exceed a small database's max_connections (a db.t3.micro allows about 60);
several containers sharing one database multiply it. The right number depends
on the database and replica count, which the image cannot know, so the
entrypoint accepts APACHE_MAX_REQUEST_WORKERS (1-2000) and writes it to
conf-enabled, raising ServerLimit when it exceeds 256. Unset keeps Apache's
default; invalid values are ignored with a warning.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- A digit string longer than 19 digits wrapped around in bash arithmetic and
  could be accepted (18446744073709553616 evaluated to 2000): strip leading
  zeros and bound the length before any arithmetic
- A failed write (for example a read-only root filesystem) still logged
  "Apache will serve at most N requests at once", because a failed redirect on
  a { } group does not report failure: write with a single printf, log an
  error and keep the default instead
- Test: boundaries (1, 256, 257, 2000), the exact file, adversarial and
  overflow input, the unwritable path, and the call placement in the
  entrypoint; loosen the awk extraction pattern
- Docs: prefork-only, memory, session connection and cron caveats

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jsokol
jsokol marked this pull request as ready for review October 27, 2026 18:19
@jsokol
jsokol enabled auto-merge October 27, 2026 18:19
@jsokol
jsokol disabled auto-merge October 27, 2026 18:19
@jsokol
jsokol merged commit 1310858 into testing Oct 27, 2026
8 checks passed
@jsokol
jsokol deleted the FEATURE-apache-max-request-workers branch October 27, 2026 18:22
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