FROM composer/composer:2-bin AS composer

FROM dunglas/frankenphp:1-php8.3-bookworm AS base

# Round 945 (Q93 (a)): Debian's security fixes are taken at build time,
# rather than waiting for the base image to be rebuilt with them. CI's image
# scan fails on a HIGH or CRITICAL vulnerability that has a fix, and without
# this a fix Debian has already released would fail it until upstream acts.
RUN apt-get update && apt-get -y upgrade && apt-get -y install \
    unzip

# Round 704 (NEEDS 76): `pdo_mysql` is in this list, and it was not.
#
# `pdo` alone is the PDO layer with no driver behind it, while
# `config/common/di/db.php` builds a `Yiisoft\Db\Mysql\Driver`. The stack
# nobody ran — `infra/docker/php.Dockerfile`, deleted in the same round —
# installed `pdo_mysql`; the stack everybody ships never did. **The two dev
# stacks had diverged on whether the system of record was reachable at all**,
# and the one that kept the driver was the one the Makefile never named.
#
# Declared rather than inherited, for the reason the COPY below gives about
# `$PHP_INI_DIR`: a base image's undeclared default is a convention standing in
# for a boundary. If frankenphp's base already carries it, this is a no-op —
# the correct price for the guarantee.
RUN install-php-extensions \
    opcache \
    mbstring \
    intl \
    dom \
    ctype \
    curl \
    phar \
    openssl \
    xml \
    xmlwriter \
    simplexml \
    pdo \
    pdo_mysql

# Round 615. Without this the images run on PHP's COMPILED defaults, because no
# php.ini existed anywhere in this repository — which put upload_max_filesize at
# 2M underneath domain constants declaring 5 MiB and 10 MiB, so neither could
# ever be reached. Copied in `base` so dev, test and prod share one chain;
# UploadLimitChainTest fails the build if this COPY is removed, since a settings
# file nothing loads is the same defect one layer along.
# The literal path rather than $PHP_INI_DIR: if that variable were ever unset
# the COPY would still succeed, writing to /conf.d/app.ini, where PHP does not
# look — a settings file that exists, loads nothing, and reports no error.
COPY docker/php/app.ini /usr/local/etc/php/conf.d/app.ini

ENV LC_ALL=C.UTF-8

#
# Development
#

FROM base AS dev
ARG USER_ID=10001
ARG GROUP_ID=10001
ARG USER_NAME=appuser
ARG GROUP_NAME=appuser

RUN install-php-extensions \
    xdebug

COPY --from=composer /composer /usr/bin/composer

# Based on https://frankenphp.dev/docs/docker/#running-as-a-non-root-user
RUN \
	groupadd --gid ${GROUP_ID} ${GROUP_NAME}; \
    useradd --gid ${GROUP_ID} --uid ${USER_ID} ${GROUP_NAME}; \
	# Add additional capability to bind to port 80 and 443 \
	setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
	# Give write access to /data/caddy and /config/caddy \
	chown -R ${USER_NAME}:${GROUP_NAME} /data/caddy && chown -R ${USER_NAME}:${GROUP_NAME} /config/caddy
USER ${USER_NAME}

COPY docker/dev/Caddyfile /etc/frankenphp/Caddyfile

#
# Production
#

FROM base AS prod-builder
COPY --from=composer /composer /usr/bin/composer

# Round 624. `WORKDIR` was absent and `composer install` below ran wherever the
# base image happened to leave it — a convention standing in for a boundary, and
# one that fails as "composer.json not found" in the one stage nothing in this
# estate builds. Stated rather than inherited.
WORKDIR /app

# Round 624: this was `COPY .. /app`, which names the PARENT of the build
# context. Docker refuses that outright — "forbidden path outside the build
# context" — whichever directory the build was pointed at, because `..` is where
# a build's world ends. Both callers set the context to `apps/web`: the Makefile
# passes `.` from there, and the dev/test compose files resolve `context: ..`
# against `docker/`, which is the same place.
#
# So the production image has never built, and the flag saying so sat in the
# register for three rounds — rounds 615 and 622 both wrote "if that is what it
# appears to be" and neither checked. `DockerBuildContextTest` checks it now.
COPY . /app
RUN --mount=type=cache,target=/tmp/cache \
    composer install --no-dev --no-progress --no-interaction --classmap-authoritative && \
    rm composer.lock composer.json

FROM base AS prod
ENV APP_ENV=prod
COPY docker/Caddyfile /etc/frankenphp/Caddyfile
# Round 952 (Cham's Q148 (1)): the start that makes this container's own TLS
# key before the web server runs; everything else passes straight through to
# the base image's entrypoint (docker/prod/serve.sh).
COPY --chmod=0755 docker/prod/serve.sh /usr/local/bin/nexhire-serve
COPY --from=prod-builder --chown=www-data:www-data /app /app
# Round 805, the first time this stage was ever run: without this line the
# container exits at start-up. Caddy, running as www-data, must write its own
# state under /data/caddy and /config/caddy, and the base image leaves both to
# root — "mkdir /data/caddy/pki: permission denied", measured. The dev stage
# has always done the same. A named volume mounted there (docker/prod/
# compose.yml) is seeded from the image, so it inherits this ownership.
RUN chown -R www-data:www-data /data/caddy /config/caddy
# Round 807 (INV-188). The base image's own check asked whether Caddy's admin
# port answered, so a release answering 500 on every page reported healthy —
# and a rolling deploy (docker/prod/compose.yml: start-first, rollback on
# failure) waits on this, and would have kept it. This asks the application:
# the database answers and every migration this release carries is applied.
# The scheduler service runs this image without a web server and turns the
# check off in its own compose definition. Round 952: it asks the plain-HTTP
# listener on the container's own loopback address, which nothing outside the
# container can reach; everything from outside arrives as TLS on 8443.
HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=3 \
    CMD curl -fsS -o /dev/null http://127.0.0.1/health/app || exit 1
USER www-data
ENTRYPOINT ["nexhire-serve"]
CMD ["--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"]
