Goal
Bring redis and the cloud-sql-proxy sidecars back to the FrankenPHP dev variants by introducing a non-root supervisord layered on top of the upstream FrankenPHP image. The upstream image (edge-docker-php) stays single-process / PID 1 for serverless production. All changes live in the edge-docker-php-dev repo.
The existing root-supervisord setup that the non-FrankenPHP dev variants inherit from upstream is out of scope — those continue to work unchanged.
Decisions (from clarifying Q&A)
| # |
Decision |
| 1 |
Install redis-server apt package in the FrankenPHP dev overlay so supervisord can run it as edge. |
| 2 |
Redis is always on. No ENABLE_REDIS toggle on the FrankenPHP dev track. |
| 3 |
FrankenPHP itself becomes a supervisord-managed program. Supervisord (running as edge) is PID 1. |
| 4 |
Supervisord socket / pidfile / log paths under /tmp so edge owns them and supervisorctl works without sudo. |
| 5 |
Scope: FrankenPHP variants only. Non-fphp dev variants keep inheriting upstream root supervisord. |
| 6 |
xdebug on/off keeps using frankenphp reload --force (no change). |
| 7 |
Cloud SQL proxy: upstream commit 2042f5c only added cloud-sql-proxy to Dockerfile.common, not Dockerfile.common-frankenphp. The dev image previously had its own COPY --from=…cloud-sql-proxy (removed in dev commit 1e406a5). On the FrankenPHP track the binary is currently missing entirely. We re-add the COPY in Dockerfile.common-frankenphp in the dev repo. If upstream FrankenPHP later gains it, the dev COPY becomes a harmless overwrite. |
| 8 |
sshd is NOT supported on the FrankenPHP dev track. Standard OpenSSH sshd requires root for PAM/privsep + chpasswd; running it under non-root supervisord is contrary to the design goal. Documented as unsupported. |
| 9 |
cron is NOT supported on the FrankenPHP dev track. Standard Debian cron must start as root (setuids to each crontab's owner). Adding a non-root scheduler (e.g. supercronic) is out of scope for this change. README mentions supercronic as a future option. |
| 10 |
Cloud SQL proxy is always on. No ENABLE_SQL_PROXY toggle on the FrankenPHP dev track. |
| 11 |
No templating engine required. Because every program is unconditionally autostart=true, the supervisord conf is fully static. No j2/jinjanator install in the dev fphp overlay; the conf is shipped as a static file in the dev repo and copied into the image at build time. The upstream non-fphp track's j2 usage is unchanged (it has 4 conditional templates and is out of scope). |
| 12 |
No stopasgroup / killasgroup on any program. The three managed programs (frankenphp, redis-server, cloud-sql-proxy) are single-binary processes that handle SIGTERM cleanly themselves; stopasgroup is meant for things like Flask debug mode or shell wrappers that don't propagate signals. Matches upstream non-fphp supervisord.conf.j2, which also doesn't use these. |
| 13 |
Supervisord's own log: logfile=/dev/null, logfile_maxbytes=0. With nodaemon=true, supervisord prints its state-change messages (INFO success: frankenphp entered RUNNING state, etc.) to its own stdout, which docker captures. Matches upstream conf exactly. |
| 14 |
No umask=002 in our conf. Upstream non-fphp uses it because that track is multi-user (nginx, www-data, edge in the edge group). The fphp dev image is single-user (every supervised process runs as edge), so umask=002 has no functional effect. supervisord's default 022 is used. |
| 15 |
dev.sh is argument-aware. If invoked with no args (the default CMD ["/dev.sh"] case) it falls back to launching supervisord. If invoked with args (e.g. docker run <image> /dev.sh bash) it forwards them through launch.sh. Pattern mirrors launch-frankenphp.sh's own three-branch arg handling. |
Current state (recap of investigation)
edge-docker-php (upstream) :*-frankenphp tags:
- Single-process model.
ENTRYPOINT ["/launch.sh"], no entrypoint shim,
no supervisord, no redis-server, no cloud-sql-proxy,
no openssh-server, no cron, no j2/jinjanator, no sudo.
launch.sh execs frankenphp run --config /etc/caddy/Caddyfile as
edge (PID 1).
edge-docker-php (upstream) non-fphp :* tags now ship cloud-sql-proxy
in the base (commit 2042f5c), with a corresponding [program:sql-proxy]
in the upstream templates/supervisord.conf.j2.
edge-docker-php-dev :*-frankenphp tags (current state, post 1e406a5):
- Inherit the upstream FrankenPHP base, then
CMD ["/dev.sh"] which
finally exec /launch.sh.
- The dev image's own
COPY --from=…cloud-sql-proxy and overlay
etc/supervisor/conf.d/sql-proxy.conf were removed on the assumption
upstream provides them. On the FrankenPHP track that assumption is
currently false — the binary is missing entirely.
edge-docker-php-dev :8.3 / :8.4 tags inherit upstream non-fphp where
supervisord runs as root and forks per-program users (edge, redis,
nginx). That is the model we are NOT replicating.
Design
Process model
Supervisord (running as edge) becomes PID 1 of the dev container and
supervises three programs, all autostart=true, all running as edge:
| Program |
Command |
frankenphp |
/usr/local/bin/frankenphp run --config /etc/caddy/Caddyfile |
redis |
/usr/bin/redis-server --save "" --dir /tmp --appendonly no --bind 127.0.0.1 --port 6379 |
sql-proxy |
/usr/local/bin/cloud-sql-proxy (binary copied via dev overlay — see decision #7) |
sshd and cron are intentionally not managed (decisions #8, #9).
The README documents ENABLE_SSH and ENABLE_CRON as unsupported on the
FrankenPHP track. ENABLE_REDIS and ENABLE_SQL_PROXY are likewise
unsupported on this track (decisions #2, #10) — both services are always
on. Users wanting toggleable services should use the non-fphp dev variants.
All programs:
stdout_logfile=/dev/stdout, stderr_logfile=/dev/stderr,
*_logfile_maxbytes=0 (so logs flow to docker logs).
autorestart=true (matching upstream supervisord patterns).
- No
stopasgroup / killasgroup (decision #12).
Supervisord configuration
A static, hand-written file shipped at etc/supervisord.conf in the dev
repo, copied to /etc/supervisord.conf at build time (owned by edge):
[supervisord]
logfile=/dev/null
logfile_maxbytes=0
pidfile=/tmp/supervisord.pid
nodaemon=true
user=edge
[unix_http_server]
file=/tmp/supervisor.sock
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock
[include]
files=/etc/supervisor/conf.d/*.conf
[program:frankenphp]
command=/usr/local/bin/frankenphp run --config /etc/caddy/Caddyfile
autostart=true
autorestart=true
user=edge
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:redis]
command=/usr/bin/redis-server --save "" --dir /tmp --appendonly no --bind 127.0.0.1 --port 6379
autostart=true
autorestart=true
user=edge
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:sql-proxy]
command=/usr/local/bin/cloud-sql-proxy --log-level=warning
autostart=true
autorestart=true
user=edge
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Notes:
user=edge in [supervisord] is harmless when supervisord is launched
as edge and prevents accidental escalation.
SERVER_NAME and SERVER_ROOT env vars are computed by the launcher
before exec'ing supervisord; supervisord inherits and passes them to
frankenphp via the normal child-process environment (no environment=
line needed since the values are in the supervisord process's env).
cloud-sql-proxy invocation matches upstream non-fphp's stanza exactly
(users supply DB instance config via env vars / extra args at deploy
time, same as today).
Logging behavior
- Child program output (frankenphp, redis, sql-proxy stdout+stderr) is
routed by supervisord directly to /dev/stdout and /dev/stderr of
the supervisord (PID 1) process — docker logs captures both streams.
- Supervisord's own activity log (state changes, restart notices)
goes to its own stdout because nodaemon=true and silent is left at
its default false. logfile=/dev/null + logfile_maxbytes=0
suppresses any file logging and is required because /dev/null is not
seekable (per the supervisor docs for non-seekable logfile paths).
- Net effect: identical to the upstream non-fphp track — all useful logs
appear in docker logs.
Boot sequence
We deliberately do not add a second launcher script that duplicates
the env-setup logic from launch-frankenphp.sh. The upstream launcher
already supports invoking arbitrary commands as PID 1 — its third arg
branch is exec "$@". We exploit that.
The new chain on the FrankenPHP dev variants:
ENTRYPOINT ["/launch.sh"] — unchanged (inherited from upstream).
CMD ["/dev.sh"] — unchanged at the Docker level.
- So the runtime invocation is
/launch.sh /dev.sh — launch.sh sources
/etc/profile.d/edge-env.sh, exports SERVER_NAME and SERVER_ROOT,
then execs /dev.sh (its third-branch exec "$@").
dev.sh:
- Sets
COMPOSER_HOME, YARN_CACHE_FOLDER, npm_config_cache (existing).
- Sources
/ona.sh if gitpod is on PATH (existing).
- Writes
$RUNTIME_URL to /tmp/runtime.url (existing).
- Toggles xdebug if
XDEBUG_ENABLE=On (existing).
- CHANGED: the existing final
exec /launch.sh "$@" is replaced
by an arg-aware pattern that mirrors launch-frankenphp.sh's own
fallback model:
# If no args were passed, default to supervisord-managed multi-proc.
# Otherwise, forward whatever was passed through launch.sh.
if [ $# -eq 0 ]; then
set -- /usr/bin/supervisord
fi
exec /launch.sh "$@"
This re-enters launch.sh with explicit args. launch.sh's env exports
are idempotent (edge-env.sh source is guarded by CUSTOM_VARS_SET;
SERVER_NAME / SERVER_ROOT re-exports are no-ops). It then execs
whatever was selected, which becomes PID 1 with all required env in
scope.
Container PID 1 in the default case is then supervisord running as
edge. No new launcher script lives in the dev repo, no upstream
changes are needed.
Invocation modes
The arg-aware dev.sh gives users three boot modes without sacrificing the
single-process escape hatch:
| Invocation |
What happens |
docker run <image> |
CMD /dev.sh runs with no args → dev.sh setup → defaults to supervisord (multi-proc, all sidecars). |
docker run <image> /dev.sh frankenphp run --config /etc/caddy/Caddyfile |
dev.sh setup → forwards args → launch.sh execs frankenphp directly (single-proc). |
docker run <image> /dev.sh bash |
dev.sh setup (composer/yarn caches, ona, xdebug, runtime URL) → interactive shell via launch.sh. |
docker run <image> bash |
Bypasses dev.sh entirely (CMD overridden) — identical to upstream behavior. |
Why this is safe to run as edge
- All sockets / pidfiles / log paths are under
/tmp (writable).
- All managed programs already run fine as
edge: frankenphp does so
upstream; cloud-sql-proxy is a userland Go binary; redis-server
doesn't need a dedicated redis user when its --dir is a writable
path (we use /tmp).
- No
CAP_NET_BIND_SERVICE needed — frankenphp listens on ${PORT}
(default 8080), redis on 127.0.0.1:6379, sql-proxy on >1024 ports.
Shutdown semantics
docker stop sends SIGTERM to PID 1 (supervisord) → supervisord sends
SIGTERM to each child → each child handles it cleanly (frankenphp
drains, redis exits, sql-proxy exits).
- Ona
stop: for the FrankenPHP track: supervisorctl shutdown (no
-c flag needed because /etc/supervisord.conf is in supervisorctl's
default search path; no sudo needed because supervisord runs as
edge and the socket is under /tmp).
xdebug toggle (no change to user behavior)
usr/local/bin/xdebug already detects frankenphp and uses
frankenphp reload --force --config /etc/caddy/Caddyfile. That keeps
working unchanged because frankenphp is still running with the same
config file — supervisord just supervises the process now.
File-by-file changes (in /workspaces/edge-docker-php-dev)
Add
etc/supervisord.conf — new static file, contents shown above.
(Path chosen to match the upstream non-fphp track which writes its
rendered conf to /etc/supervisord.conf. Lives under etc/ in the
repo so the existing COPY --chown=edge . / lands it at
/etc/supervisord.conf.)
Modify
-
Dockerfile.common-frankenphp:
- Add to apt install list (root section):
redis-server, supervisor.
(No pipx/jinjanator — decision #11.)
- Re-introduce the cloud-sql-proxy COPY (decision #7) in the root
section, before the final USER edge:
# Install Google Cloud SQL Proxy (upstream FrankenPHP base does not ship it)
COPY --from=gcr.io/cloud-sql-connectors/cloud-sql-proxy:2 \
/cloud-sql-proxy /usr/local/bin/cloud-sql-proxy
-
dev.sh:
- Replace the final
exec /launch.sh "$@" with the arg-aware
fallback pattern (decision #15):
if [ $# -eq 0 ]; then
set -- /usr/bin/supervisord
fi
exec /launch.sh "$@"
This routes through the existing upstream launcher (which sets up
SERVER_NAME / SERVER_ROOT / edge-env.sh and then execs the
command per its third arg-handling branch). No new launcher script
is needed in the dev repo.
-
README.md:
- Update the "Image variants" table — the FrankenPHP variants now use
"supervisord (multi-proc, runs as edge)" instead of "single process".
- Add a new section that outlines the "Invocation modes" mentioned above (the table with Invocation and What happens).
- Replace the FrankenPHP Ona
automations.yaml example to use a
supervisord-style start/stop:
services:
servers:
name: supervisord
description: Launches FrankenPHP, Redis and SQL Proxy
commands:
start: /dev.sh
stop: supervisorctl shutdown
triggeredBy:
- postEnvironmentStart
- Reflect that
redis and cloud-sql-proxy now run on the FrankenPHP
track too (always on, no env toggle).
- Document explicitly that
ENABLE_REDIS, ENABLE_SQL_PROXY,
ENABLE_SSH, and ENABLE_CRON are unsupported on the FrankenPHP
track. Users needing toggles or sshd/cron should use the
non-frankenphp dev variants. Mention supercronic as a future option
for non-root cron if demand arises.
Unchanged
Dockerfile.common (non-fphp dev) — out of scope.
Dockerfile.php83, Dockerfile.php84 — out of scope.
Dockerfile.php83-frankenphp, Dockerfile.php84-frankenphp — they
only INCLUDE+ Dockerfile.common-frankenphp, no edits needed.
usr/local/bin/xdebug — already handles both tracks correctly.
home/edge/.bashrc, home/edge/.bash_profile,
templates/20-xdebug.ini, ona.sh, publish.sh.
- Upstream
edge-docker-php repo — untouched. Production
single-process semantics preserved. The existing launch-frankenphp.sh
third arg-handling branch (exec "$@") already accommodates our
pattern; no upstream tweak required.
Verification plan (post-implementation, manual)
docker build -f Dockerfile.php83-frankenphp -t test:fphp .
docker run --rm -p 8080:8080 test:fphp
- Expect: supervisord starts as
edge, all three child programs
log to docker stdout. Supervisord's own
INFO success: … entered RUNNING state lines visible too.
curl localhost:8080/healthz → ok.
docker exec -it <c> supervisorctl status (no sudo) → all three
(frankenphp, redis, sql-proxy) RUNNING.
docker exec -it <c> redis-cli ping → PONG.
docker exec -it <c> which cloud-sql-proxy →
/usr/local/bin/cloud-sql-proxy (verifies the re-introduced COPY).
docker exec -it <c> xdebug on → "Xdebug enabled", frankenphp
reloads, PHP exposes xdebug; xdebug off reverses it.
docker exec <c> ps -eo user,pid,cmd → all processes are edge,
no stray root processes.
docker stop <c> exits within the grace period; logs show clean
shutdown messages from all three children.
docker run --rm -p 8080:8080 outeredge/edge-docker-php:8.3-frankenphp
(upstream) still PID-1-frankenphp, regression-free.
- Invocation-mode checks (decision #15):
docker run --rm -p 8080:8080 test:fphp /dev.sh frankenphp run --config /etc/caddy/Caddyfile
→ dev.sh setup runs (composer caches set, xdebug toggled per env,
etc.) then exec's single-proc frankenphp. ps shows frankenphp
as PID 1, no supervisord.
docker run --rm -it test:fphp /dev.sh bash → dev.sh setup runs,
drops to interactive shell. id is edge.
docker run --rm test:fphp bash -c 'echo $XDEBUG_ENABLE' →
bypasses dev.sh entirely (CMD overridden, dev.sh setup skipped).
XDEBUG_ENABLE reads from the image default Off. Confirms the
pure escape hatch is intact.
Risks / open considerations
redis-server writes to /tmp: by design — dev redis is
ephemeral. If a user wants persistence they can edit
/etc/supervisord.conf to override --dir.
- Escape hatch for single-process behavior: still works via
docker run … <image> frankenphp run --config /etc/caddy/Caddyfile
(or any other CMD override). The supervisord-managed multi-proc
behavior only engages when dev.sh runs and reaches its final exec.
Goal
Bring
redisand thecloud-sql-proxysidecars back to the FrankenPHP dev variants by introducing a non-root supervisord layered on top of the upstream FrankenPHP image. The upstream image (edge-docker-php) stays single-process / PID 1 for serverless production. All changes live in theedge-docker-php-devrepo.The existing root-supervisord setup that the non-FrankenPHP dev variants inherit from upstream is out of scope — those continue to work unchanged.
Decisions (from clarifying Q&A)
redis-serverapt package in the FrankenPHP dev overlay so supervisord can run it asedge.ENABLE_REDIStoggle on the FrankenPHP dev track.edge) is PID 1./tmpsoedgeowns them andsupervisorctlworks without sudo.xdebug on/offkeeps usingfrankenphp reload --force(no change).2042f5conly addedcloud-sql-proxytoDockerfile.common, notDockerfile.common-frankenphp. The dev image previously had its ownCOPY --from=…cloud-sql-proxy(removed in dev commit1e406a5). On the FrankenPHP track the binary is currently missing entirely. We re-add the COPY inDockerfile.common-frankenphpin the dev repo. If upstream FrankenPHP later gains it, the devCOPYbecomes a harmless overwrite.sshdis NOT supported on the FrankenPHP dev track. Standard OpenSSHsshdrequires root for PAM/privsep +chpasswd; running it under non-root supervisord is contrary to the design goal. Documented as unsupported.cronis NOT supported on the FrankenPHP dev track. Standard Debiancronmust start as root (setuids to each crontab's owner). Adding a non-root scheduler (e.g. supercronic) is out of scope for this change. README mentions supercronic as a future option.ENABLE_SQL_PROXYtoggle on the FrankenPHP dev track.autostart=true, the supervisord conf is fully static. Noj2/jinjanator install in the dev fphp overlay; the conf is shipped as a static file in the dev repo and copied into the image at build time. The upstream non-fphp track'sj2usage is unchanged (it has 4 conditional templates and is out of scope).stopasgroup/killasgroupon any program. The three managed programs (frankenphp, redis-server, cloud-sql-proxy) are single-binary processes that handle SIGTERM cleanly themselves;stopasgroupis meant for things like Flask debug mode or shell wrappers that don't propagate signals. Matches upstream non-fphpsupervisord.conf.j2, which also doesn't use these.logfile=/dev/null,logfile_maxbytes=0. Withnodaemon=true, supervisord prints its state-change messages (INFO success: frankenphp entered RUNNING state, etc.) to its own stdout, which docker captures. Matches upstream conf exactly.umask=002in our conf. Upstream non-fphp uses it because that track is multi-user (nginx, www-data, edge in the edge group). The fphp dev image is single-user (every supervised process runs asedge), soumask=002has no functional effect. supervisord's default022is used.dev.shis argument-aware. If invoked with no args (the defaultCMD ["/dev.sh"]case) it falls back to launching supervisord. If invoked with args (e.g.docker run <image> /dev.sh bash) it forwards them throughlaunch.sh. Pattern mirrorslaunch-frankenphp.sh's own three-branch arg handling.Current state (recap of investigation)
edge-docker-php(upstream):*-frankenphptags:ENTRYPOINT ["/launch.sh"], no entrypoint shim,no supervisord, no redis-server, no
cloud-sql-proxy,no
openssh-server, nocron, noj2/jinjanator, nosudo.launch.shexecsfrankenphp run --config /etc/caddy/Caddyfileasedge(PID 1).edge-docker-php(upstream) non-fphp:*tags now shipcloud-sql-proxyin the base (commit
2042f5c), with a corresponding[program:sql-proxy]in the upstream
templates/supervisord.conf.j2.edge-docker-php-dev:*-frankenphptags (current state, post1e406a5):CMD ["/dev.sh"]whichfinally
exec /launch.sh.COPY --from=…cloud-sql-proxyand overlayetc/supervisor/conf.d/sql-proxy.confwere removed on the assumptionupstream provides them. On the FrankenPHP track that assumption is
currently false — the binary is missing entirely.
edge-docker-php-dev:8.3/:8.4tags inherit upstream non-fphp wheresupervisord runs as root and forks per-program users (
edge,redis,nginx). That is the model we are NOT replicating.Design
Process model
Supervisord (running as
edge) becomes PID 1 of the dev container andsupervises three programs, all
autostart=true, all running asedge:frankenphp/usr/local/bin/frankenphp run --config /etc/caddy/Caddyfileredis/usr/bin/redis-server --save "" --dir /tmp --appendonly no --bind 127.0.0.1 --port 6379sql-proxy/usr/local/bin/cloud-sql-proxy(binary copied via dev overlay — see decision #7)sshdandcronare intentionally not managed (decisions #8, #9).The README documents
ENABLE_SSHandENABLE_CRONas unsupported on theFrankenPHP track.
ENABLE_REDISandENABLE_SQL_PROXYare likewiseunsupported on this track (decisions #2, #10) — both services are always
on. Users wanting toggleable services should use the non-fphp dev variants.
All programs:
stdout_logfile=/dev/stdout,stderr_logfile=/dev/stderr,*_logfile_maxbytes=0(so logs flow todocker logs).autorestart=true(matching upstream supervisord patterns).stopasgroup/killasgroup(decision #12).Supervisord configuration
A static, hand-written file shipped at
etc/supervisord.confin the devrepo, copied to
/etc/supervisord.confat build time (owned byedge):Notes:
user=edgein[supervisord]is harmless when supervisord is launchedas
edgeand prevents accidental escalation.SERVER_NAMEandSERVER_ROOTenv vars are computed by the launcherbefore exec'ing supervisord; supervisord inherits and passes them to
frankenphp via the normal child-process environment (no
environment=line needed since the values are in the supervisord process's env).
cloud-sql-proxyinvocation matches upstream non-fphp's stanza exactly(users supply DB instance config via env vars / extra args at deploy
time, same as today).
Logging behavior
routed by supervisord directly to
/dev/stdoutand/dev/stderrofthe supervisord (PID 1) process —
docker logscaptures both streams.goes to its own stdout because
nodaemon=trueandsilentis left atits default
false.logfile=/dev/null+logfile_maxbytes=0suppresses any file logging and is required because
/dev/nullis notseekable (per the supervisor docs for non-seekable logfile paths).
appear in
docker logs.Boot sequence
We deliberately do not add a second launcher script that duplicates
the env-setup logic from
launch-frankenphp.sh. The upstream launcheralready supports invoking arbitrary commands as PID 1 — its third arg
branch is
exec "$@". We exploit that.The new chain on the FrankenPHP dev variants:
ENTRYPOINT ["/launch.sh"]— unchanged (inherited from upstream).CMD ["/dev.sh"]— unchanged at the Docker level./launch.sh /dev.sh—launch.shsources/etc/profile.d/edge-env.sh, exportsSERVER_NAMEandSERVER_ROOT,then
execs/dev.sh(its third-branchexec "$@").dev.sh:COMPOSER_HOME,YARN_CACHE_FOLDER,npm_config_cache(existing)./ona.shifgitpodis on PATH (existing).$RUNTIME_URLto/tmp/runtime.url(existing).XDEBUG_ENABLE=On(existing).exec /launch.sh "$@"is replacedby an arg-aware pattern that mirrors
launch-frankenphp.sh's ownfallback model:
are idempotent (
edge-env.shsource is guarded byCUSTOM_VARS_SET;SERVER_NAME/SERVER_ROOTre-exports are no-ops). It thenexecswhatever was selected, which becomes PID 1 with all required env in
scope.
Container PID 1 in the default case is then
supervisordrunning asedge. No new launcher script lives in the dev repo, no upstreamchanges are needed.
Invocation modes
The arg-aware dev.sh gives users three boot modes without sacrificing the
single-process escape hatch:
docker run <image>/dev.shruns with no args → dev.sh setup → defaults to supervisord (multi-proc, all sidecars).docker run <image> /dev.sh frankenphp run --config /etc/caddy/Caddyfileexecs frankenphp directly (single-proc).docker run <image> /dev.sh bashdocker run <image> bashWhy this is safe to run as
edge/tmp(writable).edge: frankenphp does soupstream; cloud-sql-proxy is a userland Go binary;
redis-serverdoesn't need a dedicated
redisuser when its--diris a writablepath (we use
/tmp).CAP_NET_BIND_SERVICEneeded — frankenphp listens on${PORT}(default 8080), redis on 127.0.0.1:6379, sql-proxy on >1024 ports.
Shutdown semantics
docker stopsends SIGTERM to PID 1 (supervisord) → supervisord sendsSIGTERM to each child → each child handles it cleanly (frankenphp
drains, redis exits, sql-proxy exits).
stop:for the FrankenPHP track:supervisorctl shutdown(no-cflag needed because/etc/supervisord.confis in supervisorctl'sdefault search path; no
sudoneeded because supervisord runs asedgeand the socket is under/tmp).xdebug toggle (no change to user behavior)
usr/local/bin/xdebugalready detectsfrankenphpand usesfrankenphp reload --force --config /etc/caddy/Caddyfile. That keepsworking unchanged because frankenphp is still running with the same
config file — supervisord just supervises the process now.
File-by-file changes (in
/workspaces/edge-docker-php-dev)Add
etc/supervisord.conf— new static file, contents shown above.(Path chosen to match the upstream non-fphp track which writes its
rendered conf to
/etc/supervisord.conf. Lives underetc/in therepo so the existing
COPY --chown=edge . /lands it at/etc/supervisord.conf.)Modify
Dockerfile.common-frankenphp:redis-server,supervisor.(No
pipx/jinjanator— decision #11.)section, before the final
USER edge:dev.sh:exec /launch.sh "$@"with the arg-awarefallback pattern (decision #15):
SERVER_NAME/SERVER_ROOT/edge-env.shand thenexecs thecommand per its third arg-handling branch). No new launcher script
is needed in the dev repo.
README.md:"supervisord (multi-proc, runs as
edge)" instead of "single process".automations.yamlexample to use asupervisord-style start/stop:
redisandcloud-sql-proxynow run on the FrankenPHPtrack too (always on, no env toggle).
ENABLE_REDIS,ENABLE_SQL_PROXY,ENABLE_SSH, andENABLE_CRONare unsupported on the FrankenPHPtrack. Users needing toggles or sshd/cron should use the
non-frankenphp dev variants. Mention supercronic as a future option
for non-root cron if demand arises.
Unchanged
Dockerfile.common(non-fphp dev) — out of scope.Dockerfile.php83,Dockerfile.php84— out of scope.Dockerfile.php83-frankenphp,Dockerfile.php84-frankenphp— theyonly
INCLUDE+ Dockerfile.common-frankenphp, no edits needed.usr/local/bin/xdebug— already handles both tracks correctly.home/edge/.bashrc,home/edge/.bash_profile,templates/20-xdebug.ini,ona.sh,publish.sh.edge-docker-phprepo — untouched. Productionsingle-process semantics preserved. The existing
launch-frankenphp.shthird arg-handling branch (
exec "$@") already accommodates ourpattern; no upstream tweak required.
Verification plan (post-implementation, manual)
docker build -f Dockerfile.php83-frankenphp -t test:fphp .docker run --rm -p 8080:8080 test:fphpedge, all three child programslog to docker stdout. Supervisord's own
INFO success: … entered RUNNING statelines visible too.curl localhost:8080/healthz→ok.docker exec -it <c> supervisorctl status(no sudo) → all three(
frankenphp,redis,sql-proxy) RUNNING.docker exec -it <c> redis-cli ping→PONG.docker exec -it <c> which cloud-sql-proxy→/usr/local/bin/cloud-sql-proxy(verifies the re-introduced COPY).docker exec -it <c> xdebug on→ "Xdebug enabled", frankenphpreloads, PHP exposes xdebug;
xdebug offreverses it.docker exec <c> ps -eo user,pid,cmd→ all processes areedge,no stray root processes.
docker stop <c>exits within the grace period; logs show cleanshutdown messages from all three children.
docker run --rm -p 8080:8080 outeredge/edge-docker-php:8.3-frankenphp(upstream) still PID-1-frankenphp, regression-free.
docker run --rm -p 8080:8080 test:fphp /dev.sh frankenphp run --config /etc/caddy/Caddyfile→ dev.sh setup runs (composer caches set, xdebug toggled per env,
etc.) then exec's single-proc frankenphp.
psshows frankenphpas PID 1, no supervisord.
docker run --rm -it test:fphp /dev.sh bash→ dev.sh setup runs,drops to interactive shell.
idisedge.docker run --rm test:fphp bash -c 'echo $XDEBUG_ENABLE'→bypasses dev.sh entirely (CMD overridden, dev.sh setup skipped).
XDEBUG_ENABLEreads from the image defaultOff. Confirms thepure escape hatch is intact.
Risks / open considerations
redis-serverwrites to/tmp: by design — dev redis isephemeral. If a user wants persistence they can edit
/etc/supervisord.confto override--dir.docker run … <image> frankenphp run --config /etc/caddy/Caddyfile(or any other CMD override). The supervisord-managed multi-proc
behavior only engages when
dev.shruns and reaches its final exec.