diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml new file mode 100644 index 0000000..4e28f7c --- /dev/null +++ b/docker-compose-prod.yml @@ -0,0 +1,42 @@ +version: '3.7' +services: + nginx: + build: + context: ./docker/nginx + dockerfile: Dockerfile + depends_on: + - app + - swagger + ports: + - ${DOCKER_CAPTCHA_NGINX_PORT}:80 + app: + depends_on: + - redis + build: + context: ./docker/app + dockerfile: Dockerfile + target: PRODUCTION +# restart: always + cap_drop: + - ALL + cap_add: + - SETGID + - SETUID + - CHOWN + - FOWNER + ports: + - "9000" + env_file: .env + volumes: + - /etc/localtime:/etc/localtime:ro + swagger: + image: swaggerapi/swagger-ui + depends_on: + - app + environment: + URLS: "[ { url: '/swagger.json', name: '/swagger.json' } ]" + BASE_URL: /api-docs + ports: + - "8080" + redis: + image: redis:3.0-alpine diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile new file mode 100644 index 0000000..7ce2f40 --- /dev/null +++ b/docker/app/Dockerfile @@ -0,0 +1,87 @@ +FROM docker.io/php:8.2-zts-alpine3.18 AS UNIT_BUILDER + +ARG UNIT_VERSION=1.31.1 + +RUN apk --no-cache add pcre2-dev gcc git musl-dev make && \ + mkdir -p /usr/lib/unit/modules && \ + git clone https://github.com/nginx/unit.git && \ + cd unit && \ + git checkout $UNIT_VERSION && \ + ./configure --prefix=/var --statedir=/var/lib/unit --runstatedir=/var/run --control=unix:/run/unit/control.unit.sock --log=/var/log/unit.log --user=www-data --group=www-data --tmpdir=/tmp --modulesdir=/var/lib/unit/modules && \ + ./configure php && \ + make && \ + make install + +FROM docker.io/php:8.2-zts-alpine3.18 as BUILD + +COPY --from=UNIT_BUILDER /var/sbin/unitd /usr/sbin/unitd +COPY --from=UNIT_BUILDER /var/lib/unit/ /var/lib/unit/ + +COPY docker-entrypoint.sh /home/unit/docker-entrypoint.sh +COPY unit-config.json /docker-entrypoint.d/config.json + +RUN apk --no-cache add pcre2 libbz2 libpng libwebp libjpeg-turbo icu-libs freetype oniguruma libzip \ + && apk add --no-cache --virtual .phpize-deps icu-dev libpng-dev bzip2-dev libwebp-dev libjpeg-turbo-dev freetype-dev oniguruma-dev libzip-dev pcre2-dev ${PHPIZE_DEPS} \ + && docker-php-ext-configure intl --enable-intl && \ + docker-php-ext-configure bcmath --enable-bcmath && \ + docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp && \ + docker-php-ext-install -j$(nproc) gd && \ + docker-php-ext-install bcmath &&\ + docker-php-ext-install pdo \ + mysqli pdo_mysql \ + intl mbstring \ + zip pcntl \ + exif opcache bz2 \ + calendar \ + && pear update-channels && pecl update-channels \ + && pecl install redis && docker-php-ext-enable redis \ + && rm -rf /tmp/pear \ + && docker-php-source delete \ + && apk del .phpize-deps \ + && rm -rf /var/cache/apk/* && rm -rf /etc/apk/cache \ + && rm -rf /usr/share/php && rm -rf /tmp/* \ + && rm "$PHP_INI_DIR/php.ini-development" \ + && mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \ + && mkdir -p /tmp/php/upload \ + && mkdir -p /tmp/php/sys \ + && mkdir -p /tmp/php/session \ + && chown -R www-data:www-data /tmp/php \ + && ln -sf /dev/stdout /var/log/unit.log \ + && addgroup -S unit && adduser -S unit -G unit \ + && chmod 755 /home/unit/docker-entrypoint.sh + +FROM BUILD as APP_BUILD_FOR_PRODUCTION +WORKDIR /home/app +RUN apk --no-cache add git nodejs npm \ + && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \ + && git clone https://git.kor-elf.net/kor-elf/service-captcha.git . \ + && composer install --optimize-autoloader --no-dev \ + && npm install && npm run build \ + && rm -rf /home/app/node_modules /home/app/.git /home/app/docker + +# +FROM BUILD AS PRODUCTION + +COPY --from=APP_BUILD_FOR_PRODUCTION /home/app /var/www/html + +WORKDIR /var/www/html + +STOPSIGNAL SIGTERM + +ENTRYPOINT ["/home/unit/docker-entrypoint.sh"] +EXPOSE 9000 +CMD ["unitd", "--no-daemon", "--control", "unix:/var/run/control.unit.sock", "--user", "unit", "--group", "unit"] + + +# +FROM BUILD AS DEVELOP + +WORKDIR /var/www/html + +STOPSIGNAL SIGTERM + +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + +ENTRYPOINT ["/home/unit/docker-entrypoint.sh"] +EXPOSE 9000 +CMD ["unitd", "--no-daemon", "--control", "unix:/var/run/control.unit.sock", "--user", "unit", "--group", "unit"] diff --git a/docker/app/docker-entrypoint.sh b/docker/app/docker-entrypoint.sh new file mode 100644 index 0000000..0ea0a31 --- /dev/null +++ b/docker/app/docker-entrypoint.sh @@ -0,0 +1,111 @@ +#!/bin/sh + +set -euo pipefail + +WAITLOOPS=5 +SLEEPSEC=1 +unitd="unitd" + +curl_put() +{ + RET=$(/usr/bin/curl -s -w '%{http_code}' -X PUT --data-binary @$1 --unix-socket /var/run/control.unit.sock http://localhost/$2) + RET_BODY=$(echo $RET | /bin/sed '$ s/...$//') + RET_STATUS=$(echo $RET | /usr/bin/tail -c 4) + if [ "$RET_STATUS" -ne "200" ]; then + echo "$0: Error: HTTP response status code is '$RET_STATUS'" + echo "$RET_BODY" + return 1 + else + echo "$0: OK: HTTP response status code is '$RET_STATUS'" + echo "$RET_BODY" + fi + return 0 +} + +if [ "$unitd" = "unitd" ] || [ "$unitd" = "unitd-debug" ]; then + echo "$0: Launching Unit daemon to perform initial configuration..." + /usr/sbin/$unitd --control unix:/var/run/control.unit.sock + for i in $(/usr/bin/seq $WAITLOOPS); do + if [ ! -S /var/run/control.unit.sock ]; then + echo "$0: Waiting for control socket to be created..." + /bin/sleep $SLEEPSEC + else + break + fi + done + + # even when the control socket exists, it does not mean unit has finished initialisation + # this curl call will get a reply once unit is fully launched + /usr/bin/curl -s -X GET --unix-socket /var/run/control.unit.sock http://localhost/ + + if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -print -quit 2>/dev/null | /bin/grep -q .; then + echo "$0: /docker-entrypoint.d/ is not empty, applying initial configuration..." + + echo "$0: Looking for certificate bundles in /docker-entrypoint.d/..." + for f in $(/usr/bin/find /docker-entrypoint.d/ -type f -name "*.pem"); do + echo "$0: Uploading certificates bundle: $f" + curl_put $f "certificates/$(basename $f .pem)" + done + + echo "$0: Looking for JavaScript modules in /docker-entrypoint.d/..." + for f in $(/usr/bin/find /docker-entrypoint.d/ -type f -name "*.js"); do + echo "$0: Uploading JavaScript module: $f" + curl_put $f "js_modules/$(basename $f .js)" + done + + echo "$0: Looking for configuration snippets in /docker-entrypoint.d/..." + for f in $(/usr/bin/find /docker-entrypoint.d/ -type f -name "*.json"); do + echo "$0: Applying configuration $f"; + curl_put $f "config" + done + + if [ ! -z ${UNIT_SOURCE+x} ] + then + echo "[${UNIT_SOURCE}]" > /docker-entrypoint.d/unit_source.json + curl_put "/docker-entrypoint.d/unit_source.json" "config/listeners/*:9000/forwarded/source" + fi + + echo "$0: Looking for shell scripts in /docker-entrypoint.d/..." + for f in $(/usr/bin/find /docker-entrypoint.d/ -type f -name "*.sh"); do + echo "$0: Launching $f"; + "$f" + done + + # warn on filetypes we don't know what to do with + for f in $(/usr/bin/find /docker-entrypoint.d/ -type f -not -name "*.sh" -not -name "*.json" -not -name "*.pem" -not -name "*.js"); do + echo "$0: Ignoring $f"; + done + fi + + echo "$0: Stopping Unit daemon after initial configuration..." + kill -TERM $(/bin/cat /var/run/unit.pid) + + for i in $(/usr/bin/seq $WAITLOOPS); do + if [ -S /var/run/control.unit.sock ]; then + echo "$0: Waiting for control socket to be removed..." + /bin/sleep $SLEEPSEC + else + break + fi + done + if [ -S /var/run/control.unit.sock ]; then + kill -KILL $(/bin/cat /var/run/unit.pid) + rm -f /var/run/control.unit.sock + fi + + echo + echo "$0: Unit initial configuration complete; ready for start up..." + echo +fi + +php artisan config:cache +php artisan event:cache +php artisan route:cache +php artisan view:cache +php artisan migrate --force + +chown -R unit:unit /var/www/html +chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache +chmod -R 777 /var/www/html/storage /var/www/html/bootstrap/cache + +exec "$@" diff --git a/docker/app/unit-config.json b/docker/app/unit-config.json new file mode 100644 index 0000000..1b73dff --- /dev/null +++ b/docker/app/unit-config.json @@ -0,0 +1,69 @@ +{ + "listeners": { + "*:9000": { + "pass": "routes", + "forwarded": { + "client_ip": "X-Forwarded-For", + "recursive": false, + "source": [ + + ] + } + } + }, + + "routes": [ + { + "match": { + "uri": [ + "/index.php/", + "~^/index\\.php/.*", + "~\\.php$" + ] + }, + "action": { + "return": 404 + } + }, + { + "action": { + "share": "/var/www/html/public$uri", + "fallback": { + "pass": "applications/laravel" + } + } + } + ], + + "applications": { + "laravel": { + "type": "php", + "root": "/var/www/html/public", + "working_directory": "/var/www/html", + "user": "www-data", + "group": "www-data", + "script": "index.php", + "processes": { + "max": 10, + "spare": 5, + "idle_timeout": 20 + }, + "options": { + "file": "/usr/local/etc/php/php.ini", + "admin": { + "upload_tmp_dir": "/tmp/php/upload", + "sys_temp_dir": "/tmp/php/sys", + "session.save_path": "/tmp/php/session", + "open_basedir": "/var/www/html:/tmp/php:.", + "memory_limit": "256M", + "upload_max_filesize": "20M", + "post_max_size": "20M", + "expose_php": "0" + }, + "user": { + "display_errors": "0" + } + } + } + } +} diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile new file mode 100644 index 0000000..515243a --- /dev/null +++ b/docker/nginx/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:alpine3.18-slim + +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 0000000..3bf40b5 --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,36 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name captcha; + + client_max_body_size 1024M; + + root /var/www/html/public; + + location / { + location /api-docs { + proxy_pass http://swagger:8080; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + } + location / { + proxy_pass http://app:9000; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + } + } +}