############################################################################### # EspoCRM - Minimal Secure Build from Source # Multi-stage build with Alpine base images ############################################################################### # Stage 1: Build frontend assets FROM node:20-alpine AS frontend WORKDIR /build COPY package.json package-lock.json ./ RUN npm ci --ignore-scripts COPY . . RUN npx grunt # Stage 2: Install PHP dependencies FROM composer:2 AS composer WORKDIR /build COPY composer.json composer.lock ./ RUN composer install \ --no-dev \ --no-interaction \ --no-progress \ --prefer-dist \ --optimize-autoloader \ --ignore-platform-reqs # Stage 3: Minimal runtime FROM php:8.4-fpm-alpine RUN set -ex; \ apk add --no-cache \ nginx \ freetype \ libjpeg-turbo \ libwebp \ libpng \ libzip \ libpq \ libldap \ icu-libs \ unzip \ ; \ apk add --no-cache --virtual .build-deps \ freetype-dev \ libjpeg-turbo-dev \ libwebp-dev \ libpng-dev \ libzip-dev \ postgresql-dev \ openldap-dev \ icu-dev \ ; \ docker-php-ext-configure gd \ --with-freetype --with-jpeg --with-webp \ ; \ docker-php-ext-install -j$(nproc) \ gd pdo_mysql pdo_pgsql zip ldap \ exif pcntl posix bcmath intl \ ; \ apk del .build-deps; \ rm -rf /tmp/* /var/cache/apk/* # PHP hardened config RUN { \ echo 'expose_php = Off'; \ echo 'display_errors = Off'; \ echo 'display_startup_errors = Off'; \ echo 'log_errors = On'; \ echo 'memory_limit = 256M'; \ echo 'max_execution_time = 180'; \ echo 'max_input_time = 180'; \ echo 'post_max_size = 50M'; \ echo 'upload_max_filesize = 50M'; \ echo 'session.cookie_httponly = 1'; \ echo 'session.cookie_secure = 1'; \ echo 'session.use_strict_mode = 1'; \ } > "$PHP_INI_DIR/conf.d/espocrm.ini" # Nginx config RUN cat > /etc/nginx/http.d/default.conf <<'NGINX' server { listen 80; server_name _; root /var/www/html; index index.php index.html; server_tokens off; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "same-origin" always; client_max_body_size 50M; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_read_timeout 180; } location ~ /\. { deny all; } location ~ ^/(data|application|custom|vendor)/ { deny all; } location = /data/config.php { deny all; } } NGINX WORKDIR /var/www/html # Copy application COPY --chown=82:82 . . COPY --chown=82:82 --from=composer /build/vendor ./vendor COPY --chown=82:82 --from=frontend /build/client ./client # Remove dev/build files RUN rm -rf node_modules .git .github tests dev \ Gruntfile.js package.json package-lock.json \ phpstan.neon phpunit.xml .idea .vscode \ .editorconfig .gitattributes .gitignore .npmrc \ diff.js lang.js po.js jsconfig.json # Set permissions RUN set -ex; \ find . -type d -exec chmod 755 {} +; \ find . -type f -exec chmod 644 {} +; \ chmod 754 bin/command 2>/dev/null || true; \ for dir in data custom client/custom; do \ mkdir -p "$dir"; \ chown -R 82:82 "$dir"; \ done # Entrypoint COPY --chmod=755 <<'EOF' /entrypoint.sh #!/bin/sh set -e if [ -n "$ESPOCRM_DATABASE_HOST" ] && [ ! -f "data/config.php" ]; then echo "Waiting for database..." i=0 while [ $i -lt 30 ]; do if php -r "new PDO('mysql:host=${ESPOCRM_DATABASE_HOST};port=${ESPOCRM_DATABASE_PORT:-3306}','${ESPOCRM_DATABASE_USER:-root}','${ESPOCRM_DATABASE_PASSWORD}');" 2>/dev/null; then echo "Database ready. Running installer..." php bin/command install \ --databasePlatform="${ESPOCRM_DATABASE_PLATFORM:-Mysql}" \ --databaseHost="$ESPOCRM_DATABASE_HOST" \ --databasePort="${ESPOCRM_DATABASE_PORT:-3306}" \ --databaseName="${ESPOCRM_DATABASE_NAME:-espocrm}" \ --databaseUser="${ESPOCRM_DATABASE_USER:-root}" \ --databasePassword="$ESPOCRM_DATABASE_PASSWORD" \ --adminUsername="${ESPOCRM_ADMIN_USERNAME:-admin}" \ --adminPassword="${ESPOCRM_ADMIN_PASSWORD:-admin}" \ --siteUrl="${ESPOCRM_SITE_URL:-http://localhost}" \ && echo "Install complete." \ || echo "CLI install failed. Use web installer." break fi i=$((i + 1)) sleep 2 done fi php-fpm -D exec nginx -g 'daemon off;' EOF VOLUME /var/www/html/data EXPOSE 80 ENTRYPOINT ["/entrypoint.sh"]