diff --git a/.devcontainer/.bashrc b/.devcontainer/.bashrc new file mode 100644 index 00000000..12e4a4ee --- /dev/null +++ b/.devcontainer/.bashrc @@ -0,0 +1,7 @@ +# Get current Git branch +parse_git_branch() { + git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ (\1)/' +} + +# Custom PS1 prompt: green user@host, blue directory, yellow git branch +export PS1='\[\e[32m\]\u@\h\[\e[m\]:\[\e[34m\]\w\[\e[33m\]$(parse_git_branch)\[\e[m\]\$ ' \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9c378021..09c0c24a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,18 +2,19 @@ { "name": "PrimeVue Inertia", "dockerComposeFile": [ - "../docker-compose.local.yml" + "../docker-compose.dev.yml" ], "service": "laravel", "workspaceFolder": "/var/www/html", "mounts": [ - "type=bind,source=/home/${localEnv:USER}/.ssh,target=/home/sail/.ssh,readonly" + "type=bind,source=/home/${localEnv:USER}/.ssh,target=/home/www-data/.ssh,readonly" ], "customizations": { "vscode": { "extensions": [ "DEVSENSE.phptools-vscode", "MehediDracula.php-namespace-resolver", + "xdebug.php-debug", "laravel.vscode-laravel", "Vue.volar", "hollowtree.vue-snippets", @@ -35,7 +36,7 @@ } } }, - "remoteUser": "sail", + "remoteUser": "www-data", "postCreateCommand": "chown -R 1000:1000 /var/www/html 2>/dev/null || true" // "forwardPorts": [], // "runServices": [], diff --git a/.devcontainer/xdebug.ini b/.devcontainer/xdebug.ini new file mode 100644 index 00000000..e8980cf4 --- /dev/null +++ b/.devcontainer/xdebug.ini @@ -0,0 +1,6 @@ +[xdebug] +zend_extension=xdebug.so +xdebug.start_with_request=yes +xdebug.client_host=host.docker.internal +xdebug.client_port=9003 +xdebug.log=/tmp/xdebug.log \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c209d6de --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +tests +.env* +Dockerfile* +docker-*.yml +*.md \ No newline at end of file diff --git a/.env.example b/.env.example index 69b07f55..d8c7b13f 100644 --- a/.env.example +++ b/.env.example @@ -26,7 +26,7 @@ DB_CONNECTION=sqlite #DB_USERNAME= #DB_PASSWORD= -SESSION_DRIVER=file +SESSION_DRIVER=database SESSION_LIFETIME=120 SESSION_ENCRYPT=false SESSION_PATH=/ @@ -63,9 +63,9 @@ AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" -WWWGROUP=1000 -WWWUSER=1000 +USER_ID=1000 +GROUP_ID=1000 +#XDEBUG_MODE=debug -APP_PORT=8000 VITE_PORT=5173 FORWARD_DB_PORT= diff --git a/.infrastructure/s6-overlay/inertia-ssr/dependencies b/.infrastructure/s6-overlay/inertia-ssr/dependencies new file mode 100644 index 00000000..54f9367c --- /dev/null +++ b/.infrastructure/s6-overlay/inertia-ssr/dependencies @@ -0,0 +1 @@ +php-fpm \ No newline at end of file diff --git a/.infrastructure/s6-overlay/inertia-ssr/run b/.infrastructure/s6-overlay/inertia-ssr/run new file mode 100644 index 00000000..a3c4275b --- /dev/null +++ b/.infrastructure/s6-overlay/inertia-ssr/run @@ -0,0 +1,2 @@ +#!/bin/sh +exec php /var/www/html/artisan inertia:start-ssr \ No newline at end of file diff --git a/.infrastructure/s6-overlay/inertia-ssr/type b/.infrastructure/s6-overlay/inertia-ssr/type new file mode 100644 index 00000000..1780f9f4 --- /dev/null +++ b/.infrastructure/s6-overlay/inertia-ssr/type @@ -0,0 +1 @@ +longrun \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e806cccb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,77 @@ +ARG NODE_VERSION=22 + +# Base Image +# https://serversideup.net/open-source/docker-php/docs +FROM serversideup/php:8.4-fpm-nginx-alpine AS base +USER root +RUN install-php-extensions bcmath gd pgsql + +# Node Image +FROM node:${NODE_VERSION}-alpine AS node + +# Install Composer packages +FROM base AS composer +WORKDIR /var/www/html +COPY composer.json composer.lock ./ +RUN composer install --no-dev --optimize-autoloader --no-scripts --no-interaction + +# Install and bundle NPM packages +FROM node:${NODE_VERSION}-alpine AS build-assets +WORKDIR /var/www/html +COPY package*.json ./ +RUN npm ci +COPY --from=composer /var/www/html/vendor/tightenco/ziggy ./vendor/tightenco/ziggy +COPY vite.config.js ./ +COPY resources ./resources +RUN npm run build + +# Development Image +FROM base AS development +ARG USER_ID +ARG GROUP_ID +USER root +RUN apk add --no-cache curl git bash gnupg postgresql-client openssh-client \ + && apk add --no-cache --virtual .build-deps build-base autoconf \ + && install-php-extensions xdebug \ + && rm -rf /var/cache/apk/* \ + && apk del .build-deps +COPY --from=node /usr/lib /usr/lib +COPY --from=node /usr/local/lib /usr/local/lib +COPY --from=node /usr/local/include /usr/local/include +COPY --from=node /usr/local/bin /usr/local/bin +COPY .devcontainer/.bashrc /home/www-data/.bashrc +COPY .devcontainer/xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini +RUN usermod -s /bin/bash www-data \ + && git config --global --add safe.directory /var/www/html \ + && mkdir -p /home/www-data/.ssh \ + && chown www-data:www-data /home/www-data/.ssh \ + && chown www-data:www-data /home/www-data/.bashrc \ + && docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID \ + && docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID --service nginx +WORKDIR /var/www/html +USER www-data + +# Production Image +FROM base AS release +WORKDIR /var/www/html +COPY --chown=www-data:www-data --from=composer /var/www/html/vendor ./vendor +COPY --chown=www-data:www-data --from=build-assets /var/www/html/public/build ./public/build +COPY --chown=www-data:www-data . . +USER www-data + +# SSR Production Image +FROM base AS ssr-release +COPY --from=node /usr/lib /usr/lib +COPY --from=node /usr/local/lib /usr/local/lib +COPY --from=node /usr/local/include /usr/local/include +COPY --from=node /usr/local/bin /usr/local/bin +COPY --chown=www-data:www-data --chmod=755 .infrastructure/s6-overlay/inertia-ssr /etc/s6-overlay/s6-rc.d/inertia-ssr +RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/inertia-ssr \ + && chown www-data:www-data /etc/s6-overlay/s6-rc.d/user/contents.d/inertia-ssr \ + && chmod 755 /etc/s6-overlay/s6-rc.d/user/contents.d/inertia-ssr +WORKDIR /var/www/html +COPY --chown=www-data:www-data --from=composer /var/www/html/vendor ./vendor +COPY --chown=www-data:www-data --from=build-assets /var/www/html/public/build ./public/build +COPY --chown=www-data:www-data --from=build-assets /var/www/html/bootstrap/ssr ./bootstrap/ssr +COPY --chown=www-data:www-data . . +USER www-data \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b65..434df9eb 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,8 @@ namespace App\Providers; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +21,11 @@ public function register(): void */ public function boot(): void { - // + $isProduction = $this->app->isProduction(); + if ($isProduction) { + URL::forceScheme('https'); + } + // Prohibits: db:wipe, migrate:fresh, migrate:refresh, and migrate:reset + DB::prohibitDestructiveCommands($isProduction); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 6ceeee60..1c62ef38 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -29,6 +29,14 @@ BaseEncryptCookies::class => EncryptCookies::class ], ); + // TrustProxies middleware for Traefik handling assets over https + $middleware->trustProxies( + at: '*', + headers: Request::HEADER_X_FORWARDED_FOR + | Request::HEADER_X_FORWARDED_HOST + | Request::HEADER_X_FORWARDED_PORT + | Request::HEADER_X_FORWARDED_PROTO + ); }) ->withExceptions(function (Exceptions $exceptions) { $exceptions->respond(function (Response $response, Throwable $exception, Request $request) { diff --git a/docker/local/database/pgsql/create-testing-database.sql b/database/create-pg-testing-database.sql similarity index 65% rename from docker/local/database/pgsql/create-testing-database.sql rename to database/create-pg-testing-database.sql index d84dc07b..5698e801 100644 --- a/docker/local/database/pgsql/create-testing-database.sql +++ b/database/create-pg-testing-database.sql @@ -1,2 +1,3 @@ +-- create a PostgreSQL testing db for PHPUnit test suite SELECT 'CREATE DATABASE testing' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'testing')\gexec diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..f3caea09 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,56 @@ +services: + laravel: + build: + context: . + dockerfile: Dockerfile + target: development + args: + USER_ID: '${USER_ID:-1000}' + GROUP_ID: '${GROUP_ID:-1000}' + extra_hosts: + - 'host.docker.internal:host-gateway' + #ports: + #- '${VITE_PORT:-5173}:${VITE_PORT:-5173}' # Not necessarily required for dev containers (auto port forwarding) + environment: + PHP_OPCACHE_ENABLE: '1' + PHP_OPCACHE_REVALIDATE_FREQ: '0' + XDEBUG_MODE: '${XDEBUG_MODE:-off}' + volumes: + - '.:/var/www/html' + labels: + - 'traefik.enable=true' + - 'traefik.http.routers.laravel-primevue.rule=Host(`laravel-primevue.localhost`)' + - 'traefik.http.services.laravel-primevue.loadbalancer.server.port=8080' # exposed http port from serversideup image + networks: + - proxy + - laravel + depends_on: + - pgsql + + pgsql: + image: 'postgres:17' + ports: + - '${FORWARD_DB_PORT:-5432}:5432' + environment: + PGPASSWORD: '${DB_PASSWORD:-secret}' + POSTGRES_DB: '${DB_DATABASE}' + POSTGRES_USER: '${DB_USERNAME}' + POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}' + volumes: + - 'postgres-data:/var/lib/postgresql/data' + - './database/create-pg-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql' + networks: + - laravel + healthcheck: + test: [ "CMD", "pg_isready", "-q", "-d", "${DB_DATABASE}", "-U", "${DB_USERNAME}" ] + retries: 3 + timeout: 5s + +volumes: + postgres-data: + +networks: + laravel: + proxy: + name: traefik_network + external: true diff --git a/docker-compose.local.yml b/docker-compose.local.yml deleted file mode 100644 index 1df2471c..00000000 --- a/docker-compose.local.yml +++ /dev/null @@ -1,60 +0,0 @@ -services: - laravel: - build: - context: ./docker/local/web - dockerfile: Dockerfile - args: - WWWGROUP: '${WWWGROUP}' - image: sail-8.4/app - extra_hosts: - - 'host.docker.internal:host-gateway' - #ports: - #- '${APP_PORT:-80}:80' not required using Traefik - #- '${VITE_PORT:-5173}:${VITE_PORT:-5173}' Not required if using dev containers (auto forwards port to localhost) - environment: - WWWUSER: '${WWWUSER}' - LARAVEL_SAIL: 1 - XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' - XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' - IGNITION_LOCAL_SITES_PATH: '${PWD}' - volumes: - - '.:/var/www/html' - labels: - - "traefik.enable=true" - - "traefik.http.routers.laravel-primevue.rule=Host(`laravel-primevue.localhost`)" - - "traefik.http.services.laravel-primevue.loadbalancer.server.port=80" - networks: - - sail - - proxy - depends_on: - - pgsql - - pgsql: - image: 'postgres:17' - ports: - - '${FORWARD_DB_PORT:-5432}:5432' - environment: - PGPASSWORD: '${DB_PASSWORD:-secret}' - POSTGRES_DB: '${DB_DATABASE}' - POSTGRES_USER: '${DB_USERNAME}' - POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}' - volumes: - - 'laravel-primevue-pgsql:/var/lib/postgresql/data' - - './docker/local/database/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql' - networks: - - sail - healthcheck: - test: [ "CMD", "pg_isready", "-q", "-d", "${DB_DATABASE}", "-U", "${DB_USERNAME}" ] - retries: 3 - timeout: 5s - -volumes: - laravel-primevue-pgsql: - driver: local - -networks: - sail: - driver: bridge - proxy: - name: traefik_network - external: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..b3ff387c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +services: + laravel: + build: + context: . + dockerfile: Dockerfile + target: release + restart: always + env_file: '.env' + environment: + PHP_OPCACHE_ENABLE: '1' + SSL_MODE: 'mixed' + volumes: + - 'storage-public:/var/www/html/storage/app/public/' + - 'storage-sessions:/var/www/html/storage/framework/sessions' + - 'storage-logs:/var/www/html/storage/logs' + labels: + - 'traefik.enable=true' + - 'traefik.http.routers.your-app-name.rule=Host(`your-domain.com`)' + - 'traefik.http.routers.your-app-name.entrypoints=websecure' + - 'traefik.http.routers.your-app-name.tls=true' + - 'traefik.http.routers.your-app-name.tls.certresolver=letsencrypt' + - 'traefik.http.services.your-app-name.loadbalancer.server.port=8080' # exposed http port from serversideup image + # Health check + - 'traefik.http.services.your-app-name.loadbalancer.healthcheck.path=/healthcheck' + - 'traefik.http.services.your-app-name.loadbalancer.healthcheck.interval=30s' + - 'traefik.http.services.your-app-name.loadbalancer.healthcheck.timeout=5s' + - 'traefik.http.services.your-app-name.loadbalancer.healthcheck.scheme=http' + networks: + - proxy + depends_on: + - pgsql + + pgsql: + image: postgres:17 + restart: always + ports: + - '${FORWARD_DB_PORT:-5432}:5432' + environment: + POSTGRES_DB: '${DB_DATABASE}' + POSTGRES_USER: '${DB_USERNAME}' + POSTGRES_PASSWORD: '${DB_PASSWORD}' + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - proxy + healthcheck: + test: ['CMD', 'pg_isready', '-q', '-d', '${DB_DATABASE}', '-U', '${DB_USERNAME}'] + retries: 3 + timeout: 5s + +volumes: + postgres-data: + storage-public: + storage-sessions: + storage-logs: + +networks: + proxy: + name: traefik_network + external: true \ No newline at end of file diff --git a/docker/local/database/mysql/create-testing-database.sh b/docker/local/database/mysql/create-testing-database.sh deleted file mode 100644 index aeb1826f..00000000 --- a/docker/local/database/mysql/create-testing-database.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL - CREATE DATABASE IF NOT EXISTS testing; - GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%'; -EOSQL diff --git a/docker/local/web/Dockerfile b/docker/local/web/Dockerfile deleted file mode 100644 index cb0fbdc1..00000000 --- a/docker/local/web/Dockerfile +++ /dev/null @@ -1,71 +0,0 @@ -FROM ubuntu:24.04 - -LABEL maintainer="Taylor Otwell" - -ARG WWWGROUP -ARG NODE_VERSION=22 -ARG MYSQL_CLIENT="mysql-client" -ARG POSTGRES_VERSION=17 - -WORKDIR /var/www/html - -ENV DEBIAN_FRONTEND=noninteractive -ENV TZ=UTC -ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80" -ENV SUPERVISOR_PHP_USER="sail" - -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \ - echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \ - echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom - -RUN apt-get update && apt-get upgrade -y \ - && mkdir -p /etc/apt/keyrings \ - && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \ - && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \ - && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ - && apt-get update \ - && apt-get install -y php8.4-cli php8.4-dev \ - php8.4-pgsql php8.4-sqlite3 php8.4-gd \ - php8.4-curl php8.4-mongodb \ - php8.4-imap php8.4-mysql php8.4-mbstring \ - php8.4-xml php8.4-zip php8.4-bcmath php8.4-soap \ - php8.4-intl php8.4-readline \ - php8.4-ldap \ - php8.4-msgpack php8.4-igbinary php8.4-redis php8.4-swoole \ - php8.4-memcached php8.4-pcov php8.4-imagick php8.4-xdebug \ - && curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \ - && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ - && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ - && apt-get update \ - && apt-get install -y nodejs \ - && npm install -g npm \ - && npm install -g pnpm \ - && npm install -g bun \ - && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \ - && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ - && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \ - && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ - && apt-get update \ - && apt-get install -y yarn \ - && apt-get install -y $MYSQL_CLIENT \ - && apt-get install -y postgresql-client-$POSTGRES_VERSION \ - && apt-get -y autoremove \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.4 - -RUN userdel -r ubuntu -RUN groupadd --force -g $WWWGROUP sail -RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail - -COPY start-container /usr/local/bin/start-container -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf -COPY php.ini /etc/php/8.4/cli/conf.d/99-sail.ini -RUN chmod +x /usr/local/bin/start-container - -EXPOSE 80/tcp - -ENTRYPOINT ["start-container"] diff --git a/docker/local/web/php.ini b/docker/local/web/php.ini deleted file mode 100644 index 0d8ce9e2..00000000 --- a/docker/local/web/php.ini +++ /dev/null @@ -1,5 +0,0 @@ -[PHP] -post_max_size = 100M -upload_max_filesize = 100M -variables_order = EGPCS -pcov.directory = . diff --git a/docker/local/web/start-container b/docker/local/web/start-container deleted file mode 100644 index 40c55dfe..00000000 --- a/docker/local/web/start-container +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then - echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'." - exit 1 -fi - -if [ ! -z "$WWWUSER" ]; then - usermod -u $WWWUSER sail -fi - -if [ ! -d /.composer ]; then - mkdir /.composer -fi - -chmod -R ugo+rw /.composer - -if [ $# -gt 0 ]; then - if [ "$SUPERVISOR_PHP_USER" = "root" ]; then - exec "$@" - else - exec gosu $WWWUSER "$@" - fi -else - exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf -fi diff --git a/docker/local/web/supervisord.conf b/docker/local/web/supervisord.conf deleted file mode 100644 index 656da8a9..00000000 --- a/docker/local/web/supervisord.conf +++ /dev/null @@ -1,14 +0,0 @@ -[supervisord] -nodaemon=true -user=root -logfile=/var/log/supervisor/supervisord.log -pidfile=/var/run/supervisord.pid - -[program:php] -command=%(ENV_SUPERVISOR_PHP_COMMAND)s -user=%(ENV_SUPERVISOR_PHP_USER)s -environment=LARAVEL_SAIL="1" -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 diff --git a/vite.config.js b/vite.config.js index ac810ca6..23056156 100644 --- a/vite.config.js +++ b/vite.config.js @@ -45,5 +45,8 @@ export default ({ mode }) => { preview: { port: devPort, }, + ssr: { + noExternal: true, // bundle node server related files, so we don't need node_modules in production + }, }); };