# ============================================ # TeamPass Docker Image - Optimized Multi-stage Build # ============================================ # Stage 1: Composer dependencies builder # Current Composer line. The installed versions come from composer.lock, not from this # binary, so the only thing the version buys is Composer's own fixes — hence "latest # supported line" rather than an alignment with any particular developer setup. Pinned to # the minor line, never to "latest", so a future major cannot land silently. FROM composer:2.10 AS composer-builder WORKDIR /app # Copy composer files COPY composer.json composer.lock ./ # Copy local packages required by composer COPY app/includes/libraries/teampassclasses ./app/includes/libraries/teampassclasses COPY app/includes/libraries/ezimuel ./app/includes/libraries/ezimuel # Install production dependencies only RUN composer install \ --no-dev \ --no-scripts \ --no-interaction \ --optimize-autoloader \ --prefer-dist\ --ignore-platform-reqs # ============================================ # Stage 2: Final production image # ============================================ FROM php:8.3-fpm-alpine3.24 # Metadata labels LABEL maintainer="TeamPass " \ org.opencontainers.image.title="TeamPass" \ org.opencontainers.image.description="Collaborative Passwords Manager" \ org.opencontainers.image.url="https://teampass.net" \ org.opencontainers.image.source="https://github.com/nilsteampassnet/TeamPass" \ org.opencontainers.image.documentation="https://documentation.teampass.net" \ org.opencontainers.image.licenses="GPL-3.0" \ org.opencontainers.image.vendor="TeamPass" # Build arguments # The CI workflow overrides this with the value it reads from app/config/include.php, # so every published image reports the version it actually contains (a branch build # used to be labelled "master"). The default only serves local builds, so it must stay # equal to TP_VERSION.TP_VERSION_MINOR; the release procedure bumps it in the same # commit as the version constants. ARG TEAMPASS_VERSION=3.2.2.5 ENV TEAMPASS_VERSION=${TEAMPASS_VERSION} # Apply the Alpine security updates published since the base image was built. # # openssl and curl are not installed below - they come with php:8.3-fpm-alpine3.24 - so # nothing here ever refreshed them, and the image shipped whatever the base image froze. # That is what accumulated as Trivy alerts (libcrypto3/libssl3/openssl and curl/libcurl), # all of them fixed upstream in the very branch the image already tracks. # # The trade-off is deliberate: the build stops being byte-reproducible across time, in # exchange for never publishing an image with known-vulnerable OS packages. Pinning the # fixed versions instead would keep reproducibility but has to be edited at every CVE. # # Placed AFTER the version ARG on purpose: that argument changes at every release, so this # layer is invalidated with it and a release build always resolves fresh packages. RUN apk upgrade --no-cache # Install system dependencies and PHP extensions RUN apk add --no-cache \ # System packages nginx \ supervisor \ busybox-suid \ netcat-openbsd \ # Libraries for PHP extensions gnu-libiconv \ libldap \ gmp \ icu-libs \ libzip \ freetype \ libjpeg-turbo \ libpng \ libxml2 \ oniguruma \ && apk add --no-cache --virtual .build-deps \ # Build dependencies $PHPIZE_DEPS \ openldap-dev \ gmp-dev \ icu-dev \ libzip-dev \ freetype-dev \ libjpeg-turbo-dev \ libpng-dev \ libxml2-dev \ oniguruma-dev \ # Configure and install PHP extensions && docker-php-ext-configure gd \ --with-freetype \ --with-jpeg \ && docker-php-ext-configure ldap \ && docker-php-ext-install -j$(nproc) \ mysqli \ pdo_mysql \ bcmath \ ldap \ gmp \ gd \ zip \ intl \ opcache \ mbstring \ xml \ # Cleanup build dependencies && apk del .build-deps \ && rm -rf /var/cache/apk/* /tmp/* /var/tmp/* # No LD_PRELOAD of preloadable_libiconv.so on purpose: Alpine has not shipped that # file for several releases. The gnu-libiconv package only provides the gnu-iconv # binary, and gnu-libiconv-libs provides libiconv.so.2, which exports the prefixed # symbols (libiconv_open, …) and therefore cannot shadow musl's iconv. Setting the # classic workaround would only make every process log an ld.so error. PHP uses # musl's iconv. # Copy PHP configuration COPY docker/php/php.ini /usr/local/etc/php/conf.d/teampass.ini # Copy Nginx configuration COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf COPY docker/nginx/teampass.conf /etc/nginx/http.d/default.conf # Copy Supervisor configuration COPY docker/supervisor/supervisord.conf /etc/supervisor/supervisord.conf # Create application directory WORKDIR /var/www/html # Copy application files COPY --chown=nginx:nginx . . # Copy vendor from composer builder COPY --from=composer-builder --chown=nginx:nginx /app/app/vendor ./app/vendor # Create required directories with proper permissions RUN mkdir -p \ storage/sk \ storage/files \ storage/upload \ storage/config \ storage/backups \ secrets \ app/includes/libraries/csrfp/log \ /var/lib/nginx/tmp \ /var/log/supervisor \ /run/nginx \ && chown -R nginx:nginx \ storage \ storage/sk \ storage/files \ storage/upload \ storage/config \ storage/backups \ secrets \ app/includes/libraries/csrfp/log \ /var/lib/nginx \ /var/log \ /run/nginx \ && chmod 700 storage/sk secrets \ && chmod 750 storage storage/files storage/upload storage/config storage/backups app/includes/libraries/csrfp/log # Remove unnecessary files for production RUN rm -rf \ .git \ .github \ tests \ .gitignore \ .dockerignore \ .scrutinizer.yml \ .codacy.yml \ .eslintrc \ teampass-docker-start.sh \ Dockerfile # Setup cron for TeamPass scheduler RUN echo "* * * * * php /var/www/html/app/sources/scheduler.php > /dev/null 2>&1" > /var/spool/cron/crontabs/nginx \ && chmod 600 /var/spool/cron/crontabs/nginx # Copy and set entrypoint script COPY docker/docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh # Health check HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1 # Expose HTTP port EXPOSE 80 # Define volumes for persistent data. # storage/config holds the install state (settings.php, csrfp.config.php) and # secrets holds the Defuse master key: both must persist across container # recreation, otherwise TeamPass would try to reinstall itself (issue #5236). VOLUME ["/var/www/html/storage/sk", "/var/www/html/storage/files", "/var/www/html/storage/upload", "/var/www/html/storage/config", "/var/www/html/secrets"] # Set entrypoint and default command ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]