Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/container-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ jobs:
- name: simplerisk (full-stack) generator
run: ./simplerisk/test_generate_dockerfile.sh

- name: simplerisk-minimal Apache worker setting
run: ./simplerisk-minimal/test_set_apache_workers.sh

- name: Fail if a generator left the committed Dockerfile modified
run: |
set -euo pipefail
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ The entrypoint script handles:
| `SIMPLERISK_DB_USERNAME/PASSWORD/DATABASE` | DB credentials |
| `SIMPLERISK_CRON_SETUP` | Enable/disable PHP cron (default: enabled) |
| `SIMPLERISK_CSRF_SECRET` | Override auto-generated CSRF secret |
| `APACHE_MAX_REQUEST_WORKERS` | Optional cap (1-2000) on simultaneous Apache workers, written by the entrypoint's `set_apache_workers`; unset keeps Apache's default of 150. Each request holds a database connection, so size it against the database's `max_connections`, the replica count, the cron job and the session connection (`SIMPLERISK_DB_FOR_SESSIONS`), and the container's memory. Invalid values are ignored with a warning; a failed write logs an error and keeps the default |
| `ADMIN_USERNAME` | Username for initial admin user (only during `DB_SETUP`; idempotent) |
| `ADMIN_PASSWORD` | Password for initial admin user |
| `ADMIN_EMAIL` | Email for initial admin user |
Expand Down
1 change: 1 addition & 0 deletions simplerisk-minimal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ docker run -d --name simplerisk -e SIMPLERISK_DB_PASSWORD=pass -e SIMPLERISK_DB_
| `SIMPLERISK_DB_SSL_CERT_PATH` | Empty string (`''`) | Path where SSL certificates, to contact the database, are located |
| `SIMPLERISK_CRON_SETUP` | `enabled` | Install the cron job to run in this Docker container |
| `SIMPLERISK_CSRF_SECRET` | Auto-generated | Override the auto-generated CSRF secret |
| `APACHE_MAX_REQUEST_WORKERS` | Apache default (150) | Maximum number of requests Apache serves at once (a whole number from 1 to 2000). Each request holds a database connection, so set this so that `replicas x APACHE_MAX_REQUEST_WORKERS` stays under the database's `max_connections`, leaving room for the cron jobs and for the second connection used when `SIMPLERISK_DB_FOR_SESSIONS` is `true`. Each worker is a full PHP process, so also keep it within the container's memory limit. It applies to the Apache prefork MPM the image ships with. An invalid value is ignored with a warning in the container log |
| `ADMIN_USERNAME` | — | Username for an initial admin user created once during `DB_SETUP`. Requires `ADMIN_PASSWORD` and `ADMIN_EMAIL`. Skipped if the username already exists |
| `ADMIN_PASSWORD` | — | Password for the initial admin user |
| `ADMIN_EMAIL` | — | Email address for the initial admin user |
Expand Down
52 changes: 52 additions & 0 deletions simplerisk-minimal/common/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,56 @@ set_ssl_certificate(){
exec_cmd "chmod 600 $key" "Failed to set permissions on TLS private key. Exiting."
}

set_apache_workers(){
# Optional cap on how many requests Apache serves at once. Every request
# holds a database connection, so the default of 150 workers can exceed a
# small database's max_connections (a db.t3.micro allows about 60), and
# several containers sharing one database multiply it. The right number
# depends on the database and the replica count, which the image cannot
# know, so it is left to the operator. Unset keeps Apache's default.
local workers="${APACHE_MAX_REQUEST_WORKERS:-}"
local conf="${APACHE_MPM_CONF_PATH:-/etc/apache2/conf-enabled/simplerisk-mpm.conf}"

# Remove what a previous start wrote, so unsetting the variable on a
# restarted container goes back to Apache's default
rm -f "$conf"

if [ -z "$workers" ]; then
return 0
fi

# Strip leading zeros and bound the length before any arithmetic: bash
# arithmetic wraps at 64 bits, so a very long digit string could otherwise
# wrap into the valid range
local digits="${workers#"${workers%%[!0]*}"}"

if ! [[ "$workers" =~ ^[0-9]+$ ]] || [ -z "$digits" ] || [ "${#digits}" -gt 4 ] || [ "$digits" -gt 2000 ]; then
print_log "apache_setup:warn" "APACHE_MAX_REQUEST_WORKERS must be a whole number between 1 and 2000 -- leaving Apache's default."
return 0
fi

workers="$digits"

local content="# Written by the container entrypoint from APACHE_MAX_REQUEST_WORKERS"$'\n'"<IfModule mpm_prefork_module>"$'\n'
# ServerLimit defaults to 256 and silently caps MaxRequestWorkers
if [ "$workers" -gt 256 ]; then
content+=$'\t'"ServerLimit ${workers}"$'\n'
fi
content+=$'\t'"MaxRequestWorkers ${workers}"$'\n'"</IfModule>"

# Do not claim a cap that was not applied: if the file cannot be written
# (for example a read-only root filesystem), say so and keep the default.
# A single printf is used because a failed redirect on a { } group does not
# reliably report failure.
if ! printf '%s\n' "$content" > "$conf"; then
print_log "apache_setup:error" "Could not write ${conf} -- leaving Apache's default."
rm -f "$conf" || true
return 0
fi

print_log "apache_setup:info" "Apache will serve at most ${workers} requests at once."
}

set_cron(){
# If SIMPLERISK_CRON_SETUP was passed and it is set to disabled
if [[ -n "${SIMPLERISK_CRON_SETUP:-}" && "${SIMPLERISK_CRON_SETUP:-}" = disabled* ]]; then
Expand Down Expand Up @@ -495,6 +545,8 @@ _main() {
print_log "initial_setup:info" "No DB env vars provided; config.php will not be written. The SimpleRisk web installer will run at first request."
fi

set_apache_workers

set_cron

if [[ -n ${DB_SETUP:-} ]]; then
Expand Down
77 changes: 77 additions & 0 deletions simplerisk-minimal/test_set_apache_workers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/bin/bash
# Exercises set_apache_workers() from common/entrypoint.sh without starting
# Apache: the function is extracted and run against a temporary conf path.
# Usage: ./test_set_apache_workers.sh

set -eo pipefail

here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

# print_log is what the entrypoint defines; capture its output so the tests
# can assert on what an operator would see
print_log(){ echo "[$1] $2"; }

# Pull just the function out of the entrypoint (from its header to the first
# closing brace at column 0). Fails loudly if the function moves or is renamed.
func="$(awk '/^set_apache_workers\(\) *\{/{p=1} p{print} p&&/^}/{exit}' "$here/common/entrypoint.sh")"
[ -n "$func" ] || { echo "FAIL: set_apache_workers not found in entrypoint.sh"; exit 1; }
eval "$func"

export APACHE_MPM_CONF_PATH="$tmp/simplerisk-mpm.conf"
failures=0
log=""

check(){
local name="$1"; shift
if "$@"; then echo "ok $name"; else echo "FAIL $name"; failures=$((failures+1)); fi
}

# run <value>: run the function in a subshell with the variable set to <value>
# (or unset for an empty argument) and keep what it logged in $log
run(){ log="$( unset APACHE_MAX_REQUEST_WORKERS; [ -z "${1:-}" ] || export APACHE_MAX_REQUEST_WORKERS="$1"; set_apache_workers 2>&1 )"; }

has(){ grep -q -- "$1" "$APACHE_MPM_CONF_PATH"; }
hasnt(){ ! grep -q -- "$1" "$APACHE_MPM_CONF_PATH"; }
absent(){ [ ! -e "$APACHE_MPM_CONF_PATH" ]; }
logged(){ [[ "$log" == *"$1"* ]]; }

expected_300=$'# Written by the container entrypoint from APACHE_MAX_REQUEST_WORKERS\n<IfModule mpm_prefork_module>\n\tServerLimit 300\n\tMaxRequestWorkers 300\n</IfModule>'

run "" ; check "unset writes nothing" absent
run 40 ; check "40 sets MaxRequestWorkers" has "MaxRequestWorkers 40"
run 40 ; check "40 leaves ServerLimit alone" hasnt ServerLimit
run 40 ; check "40 logs the cap" logged "at most 40"
run 1 ; check "lower bound 1 is accepted" has "MaxRequestWorkers 1$"
run 256 ; check "256 does not raise ServerLimit" hasnt ServerLimit
run 257 ; check "257 raises ServerLimit" has "ServerLimit 257"
run 2000 ; check "upper bound 2000 is accepted" has "MaxRequestWorkers 2000"
run 300 ; check "300 writes the exact expected file" test "$(cat "$APACHE_MPM_CONF_PATH")" = "$expected_300"
run 0040 ; check "leading zeros are normalised" has "MaxRequestWorkers 40$"
run 000000000000000000000040 ; check "long zero padding is normalised" has "MaxRequestWorkers 40$"
run "" ; check "unsetting removes the earlier file" absent

# Rejected values: no file, a warning, and never a claim that a cap was applied
for bad in abc 0 00 2001 -5 1.5 " 40" "40 " "4 0" 18446744073709553616 99999999999999999999 "40; rm -rf /" '$(touch x)' '`id`' $'40\n41' '٣'; do
run "$bad"
check "rejects '${bad//$'\n'/\\n}'" bash -c '[ ! -e "$1" ]' _ "$APACHE_MPM_CONF_PATH"
check " and warns instead of claiming a cap" bash -c '[[ "$1" == *"[apache_setup:warn]"* && "$1" != *"at most"* ]]' _ "$log"
done

# A conf path that cannot be written: say so, write nothing, and do not claim the cap
saved="$APACHE_MPM_CONF_PATH"
export APACHE_MPM_CONF_PATH="$tmp/no-such-dir/simplerisk-mpm.conf"
run 40
check "unwritable path logs an error" logged "[apache_setup:error]"
check "unwritable path does not claim a cap" bash -c '[[ "$1" != *"at most"* ]]' _ "$log"
check "unwritable path leaves no file" absent
export APACHE_MPM_CONF_PATH="$saved"

# The entrypoint must actually call the function, before it hands off to Apache
call_line="$(grep -n '^[[:space:]]*set_apache_workers$' "$here/common/entrypoint.sh" | head -1 | cut -d: -f1)"
exec_line="$(grep -n '^[[:space:]]*exec "\$@"' "$here/common/entrypoint.sh" | tail -1 | cut -d: -f1)"
check "entrypoint calls set_apache_workers before exec" bash -c '[ -n "$1" ] && [ -n "$2" ] && [ "$1" -lt "$2" ]' _ "$call_line" "$exec_line"

[ "$failures" -eq 0 ] || { echo "$failures check(s) failed"; exit 1; }
echo "all checks passed"
Loading