Closed fideloper closed 10 months ago
@fideloper I saw your article about using Unit, and I had a few notes. I figured this was probably a good place to leave them.
First, I'm curious if you looked at the Dockerfile that Unit uses for their official images: https://github.com/nginx/unit/blob/master/pkg/docker/Dockerfile.php8.2
One major difference is that they start from an official PHP base image, then build unit from source.
Another difference is in their entrypoint script. On the initial container startup, they check for configuration files, scripts, and certificates, and add them to the unit configuration. This makes it possible for a single Unit image to be easily used in a variety of situations.
I've been using unit in production for a few months, and here's my configuration.
First, I have two separate "base" images I build, since they don't change very often. One is Unit, and the other is PHP CLI:
ARG UNIT_VERSION=1.31.0
ARG PHP_VERSION=8.2
FROM unit:${UNIT_VERSION}-php${PHP_VERSION} as unit-base
RUN mkdir -p /srv/app
WORKDIR /srv/app
ARG EXT_INSTALLER_VERSION=2.1.38
COPY support/docker/install-extensions.sh /usr/local/bin/install-extensions
RUN sh /usr/local/bin/install-extensions "${EXT_INSTALLER_VERSION}"
ARG PHP_VERSION=8.2
FROM php:${PHP_VERSION}-alpine
RUN mkdir -p /srv/app
WORKDIR /srv/app
ARG EXT_INSTALLER_VERSION=2.1.38
COPY support/docker/install-extensions.sh /usr/local/bin/install-extensions
RUN sh /usr/local/bin/install-extensions "${EXT_INSTALLER_VERSION}"
Here's what install-extensions.sh
looks like:
#!/bin/sh
EXT_INSTALLER_VERSION=$1
if [ -z "$1" ]; then
EXT_INSTALLER_URL="https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions"
else
EXT_INSTALLER_URL="https://github.com/mlocati/docker-php-extension-installer/releases/download/${EXT_INSTALLER_VERSION}/install-php-extensions"
fi
set -x
curl -sSLf -o /usr/local/bin/install-php-extensions "${EXT_INSTALLER_URL}"
chmod +x /usr/local/bin/install-php-extensions
install-php-extensions ctype gd gmp intl mbstring pcntl pdo_pgsql redis soap xsl zip
php -m
Relevant to this ~tweet~ X post, I had the same problem until I discovered https://github.com/mlocati/docker-php-extension-installer, which is a wonderful tool.
ARG CI_REGISTRY_IMAGE
ARG BASE_IMAGE_TAG=develop
FROM ${CI_REGISTRY_IMAGE}/cli-base:${BASE_IMAGE_TAG} as phpbuild
RUN mkdir -p /srv/app
WORKDIR /srv/app
COPY --from=composer /usr/bin/composer /usr/bin/composer
COPY . /srv/app
RUN composer install --prefer-dist --no-dev --optimize-autoloader --no-interaction && \
php artisan horizon:publish
## Install Node.js dependencies and build assets
FROM node:18-alpine as jsbuild
RUN mkdir -p /srv/app
WORKDIR /srv/app
COPY package.json /srv/app/
COPY package-lock.json /srv/app/
RUN npm ci
COPY webpack.mix.js /srv/app/
COPY ./lang/. /srv/app/lang/
COPY ./resources/. /srv/app/resources/
RUN mkdir -p /srv/app/public && \
npx browserslist@latest --update-db && \
npm run prod
## Nginx Unit runner for web backend
ARG CI_REGISTRY_IMAGE
ARG BASE_IMAGE_TAG=develop
FROM ${CI_REGISTRY_IMAGE}/unit-base:${BASE_IMAGE_TAG} as web
RUN mkdir -p /srv/app
WORKDIR /srv/app
EXPOSE 80
COPY ./support/docker/unit/* /docker-entrypoint.d/
COPY --chown=unit:unit --from=phpbuild /srv/app/. /srv/app/.
COPY --chown=unit:unit --from=jsbuild /srv/app/public/. /srv/app/public/.
# PHP runner for queues and scheduled tasks
ARG CI_REGISTRY_IMAGE
ARG BASE_IMAGE_TAG=develop
FROM ${CI_REGISTRY_IMAGE}/cli-base:${BASE_IMAGE_TAG} as artisan
RUN mkdir -p /srv/app
WORKDIR /srv/app
COPY ./support/docker/start.sh /usr/local/bin/start.sh
ENTRYPOINT [ "/usr/local/bin/start.sh" ]
COPY --from=phpbuild /srv/app/. /srv/app/.
Having separate unit
and cli
containers is probably not necessary, but my boss liked the idea, so that's what I did.
A very important line in the file above is this one: COPY ./support/docker/unit/* /docker-entrypoint.d/
In my repository, I include just the following configuration file in there (I added some comments here, they need to be removed to make the file valid JSON):
{
"listeners": {
"*:80": {
"pass": "routes",
"forwarded": {
"protocol": "X-Forwarded-Proto",
"client_ip": "X-Forwarded-For",
"recursive": true,
"source": [
"172.17.0.0/16", // Docker internal network IPv4
"fd00:0932::/48", // Docker internal network IPv6
// Cloudflare networks
"173.245.48.0/20",
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"141.101.64.0/18",
"108.162.192.0/18",
"190.93.240.0/20",
"188.114.96.0/20",
"197.234.240.0/22",
"198.41.128.0/17",
"162.158.0.0/15",
"104.16.0.0/13",
"104.24.0.0/14",
"172.64.0.0/13",
"131.0.72.0/22",
"2400:cb00::/32",
"2606:4700::/32",
"2803:f800::/32",
"2405:b500::/32",
"2405:8100::/32",
"2a06:98c0::/29",
"2c0f:f248::/32"
]
}
}
},
"access_log": {
"path": "/srv/app/storage/logs/access.log",
"format": "$remote_addr - - [$time_local] \"$request_line\" $status $body_bytes_sent \"$header_referer\" \"$header_user_agent\" $header_cf_connecting_ip"
},
"applications": {
"laravel": {
"type": "php",
"root": "/srv/app/public/",
"script": "index.php",
"processes": {
"max": 100,
"spare": 10,
"idle_timeout": 60
}
}
},
"routes": [
{
"match": {
"uri": "!/index.php"
},
"action": {
"share": "/srv/app/public$uri",
"fallback": {
"pass": "applications/laravel"
}
}
}
]
}
I also use a Docker Bind Mount (in docker-compose.yml) to include a shell script in the configuration directory at runtime. Right now, that script looks like this:
#!/bin/sh
curl -X PUT -d '{"user": {"memory_limit": "384M", "max_execution_time": "120"}}' --unix-socket /var/run/control.unit.sock 'http://localhost/config/applications/laravel/options'
This allows easy modification of the unit settings that persists when the container is updated.
For example, your comment about "protecting dot files or other routes" can probably be done with unit's routes.NAME.match.uri
pattern-matching and `routes.NAME.action.return" set to 403 (or any other code). You can do this in the original container creation, or with a script as shown here.
One thing I plan to do with a shell script (but haven't had a chance to test yet) is loading the docker and Cloudflare IP ranges dynamically instead of prepopulating them in the initial setup.
Also, regarding your comment about setting cache headers for static assets, there are examples of doing exactly that in the documentation: https://unit.nginx.org/configuration/#response-headers - is there something more than that which you are trying to do?
@kohenkatz Thank you, that's super helpful! I appreciate you taking the time to write that all out!
I'm merging what I have into main
for now, but the real treat there is finding a way to use official PHP images and add in modules fairly easily. Keeping that bookmarked! (along with the ideas you have on nginx unit of course!)
Notes:
--copy-from
appears to be fine - I can just grab the PHP .so file and nothing else for the final image built. I'm not 100% confident there's no edge cases with that, however. But seems reasonable that it would Just Work™