diff --git a/.dockerignore b/.dockerignore index fc74dcd..2dae3eb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,21 +1,26 @@ -node_modules -.next -generated .git -.gitea +.github +.idea +.vscode + +# The previous Next.js application is intentionally outside this build. +legacy-nextjs + +node_modules +vendor .env .env.* !.env.example -!.env.local.example -test-results -playwright-report -blob-report -playwright/.cache -coverage +storage/logs/* +storage/framework/cache/data/* +storage/framework/sessions/* +storage/framework/views/* +bootstrap/cache/*.php +public/build +docker-compose*.yml +docker-compose*.yaml +*.log +tests e2e docs -*.md -.vscode -.idea -.DS_Store -npm-debug.log* +templates diff --git a/.env.example b/.env.example index 9546af7..e3e82cd 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ APP_NAME=NextChair APP_ENV=local APP_KEY= APP_DEBUG=true -APP_URL=http://localhost:8000 +APP_URL=http://localhost:8085 APP_DEFAULT_BUSINESS_SLUG= APP_LOCALE=en @@ -22,11 +22,16 @@ LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug DB_CONNECTION=mysql -DB_HOST=127.0.0.1 +DB_HOST=db DB_PORT=3306 DB_DATABASE=nextchair -DB_USERNAME=root -DB_PASSWORD= +DB_USERNAME=nextchair +DB_PASSWORD=change-me +DB_ROOT_PASSWORD=change-me-root + +# Docker publishes Nginx on this local bind address by default. +NEXTCHAIR_HTTP_BIND=127.0.0.1 +NEXTCHAIR_HTTP_PORT=8085 SESSION_DRIVER=database SESSION_LIFETIME=120 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..18b999c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,83 @@ +# syntax=docker/dockerfile:1 + +FROM node:22-bookworm-slim AS frontend + +WORKDIR /var/www/html + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY resources ./resources +COPY public ./public +COPY vite.config.js . + +RUN npm run build + + +FROM composer:2 AS composer + + +FROM php:8.4-fpm-bookworm AS app + +ENV COMPOSER_ALLOW_SUPERUSER=1 \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 + +WORKDIR /var/www/html + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + gosu \ + libfreetype6-dev \ + libicu-dev \ + libjpeg62-turbo-dev \ + libonig-dev \ + libpng-dev \ + libxml2-dev \ + libzip-dev \ + unzip \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j"$(nproc)" \ + bcmath \ + exif \ + gd \ + intl \ + mbstring \ + opcache \ + pcntl \ + pdo_mysql \ + xml \ + zip \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +COPY . . +COPY --from=frontend /var/www/html/public/build ./public/build + +RUN composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist \ + && mkdir -p \ + storage/framework/cache/data \ + storage/framework/sessions \ + storage/framework/views \ + storage/logs \ + bootstrap/cache \ + && chown -R www-data:www-data storage bootstrap/cache \ + && chmod -R ug+rwX storage bootstrap/cache + +COPY docker/entrypoint.sh /usr/local/bin/nextchair-entrypoint +RUN chmod +x /usr/local/bin/nextchair-entrypoint + +ENTRYPOINT ["/usr/local/bin/nextchair-entrypoint"] +CMD ["php-fpm", "-F"] + + +FROM nginx:1.27-alpine AS nginx + +WORKDIR /var/www/html + +COPY public ./public +COPY --from=frontend /var/www/html/public/build ./public/build +COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf + +RUN mkdir -p /var/www/html/storage/app/public \ + && ln -s /var/www/html/storage/app/public /var/www/html/public/storage diff --git a/README.md b/README.md index 78dd201..bdfbc33 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,9 @@ The application is being rebuilt as a conventional Laravel web application using PHP, MySQL, Blade, Tailwind CSS, Alpine.js, and Vite. The audited Next.js implementation is preserved in [`legacy-nextjs`](legacy-nextjs) -as the functional and visual migration reference. Deployment-specific files -from the previous implementation are retained there for reference only; the -Laravel application does not manage Docker, Caddy, Nginx, or server -provisioning. +as the functional and visual migration reference. The Laravel application has +its own deployment-neutral Docker option at the repository root; files inside +`legacy-nextjs` are not used by it. ## Local development @@ -26,6 +25,51 @@ application resolves a business from the request hostname through the `business_domains` table. Local development can use a configured local domain or the `APP_DEFAULT_BUSINESS_SLUG` fallback. +## Docker deployment + +The root `compose.yaml` runs Laravel with PHP 8.4 FPM, Nginx, MySQL 8, +the database-backed queue worker, and the scheduler. It does not include or +configure Caddy; an existing reverse proxy can forward traffic to Nginx on +`127.0.0.1:8085`. + +First run: + +```bash +cp .env.example .env +``` + +Set private values in `.env`, including: + +```dotenv +APP_URL=http://localhost:8085 +APP_KEY= +DB_HOST=db +DB_DATABASE=nextchair +DB_USERNAME=nextchair +DB_PASSWORD=replace-with-a-private-password +DB_ROOT_PASSWORD=replace-with-a-private-root-password +``` + +Then build and start the stack: + +```bash +docker compose build +docker compose up -d +docker compose exec app php artisan key:generate +docker compose exec app php artisan migrate +``` + +Migrations are intentionally not run by container startup. To inspect the +application logs: + +```bash +docker compose logs -f app nginx queue scheduler db +``` + +The default host binding is `127.0.0.1:8085:80`. Override it with +`NEXTCHAIR_HTTP_BIND` and `NEXTCHAIR_HTTP_PORT` in `.env` when needed. MySQL +has no published host port and is reachable only as `db:3306` inside Compose. + ## Verification ```bash diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..da96f36 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,102 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + target: app + image: nextchair-laravel:latest + restart: unless-stopped + env_file: + - .env + environment: + APP_ENV: ${APP_ENV:-production} + APP_DEBUG: ${APP_DEBUG:-false} + APP_URL: ${APP_URL:-http://localhost:8085} + DB_CONNECTION: mysql + DB_HOST: db + DB_PORT: 3306 + DB_DATABASE: ${DB_DATABASE:-nextchair} + DB_USERNAME: ${DB_USERNAME:-nextchair} + DB_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD in .env} + depends_on: + db: + condition: service_healthy + volumes: + - nextchair_storage:/var/www/html/storage + + nginx: + build: + context: . + dockerfile: Dockerfile + target: nginx + image: nextchair-nginx:latest + restart: unless-stopped + depends_on: + - app + ports: + - "${NEXTCHAIR_HTTP_BIND:-127.0.0.1}:${NEXTCHAIR_HTTP_PORT:-8085}:80" + volumes: + - nextchair_storage:/var/www/html/storage:ro + + db: + image: mysql:8.4 + restart: unless-stopped + environment: + MYSQL_DATABASE: ${DB_DATABASE:-nextchair} + MYSQL_USER: ${DB_USERNAME:-nextchair} + MYSQL_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD in .env} + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?Set DB_ROOT_PASSWORD in .env} + volumes: + - nextchair_mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD --silent"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 20s + + queue: + image: nextchair-laravel:latest + restart: unless-stopped + env_file: + - .env + environment: + APP_ENV: ${APP_ENV:-production} + APP_DEBUG: ${APP_DEBUG:-false} + DB_CONNECTION: mysql + DB_HOST: db + DB_PORT: 3306 + DB_DATABASE: ${DB_DATABASE:-nextchair} + DB_USERNAME: ${DB_USERNAME:-nextchair} + DB_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD in .env} + command: ["php", "artisan", "queue:work", "--sleep=3", "--tries=3", "--timeout=90"] + depends_on: + db: + condition: service_healthy + volumes: + - nextchair_storage:/var/www/html/storage + + scheduler: + image: nextchair-laravel:latest + restart: unless-stopped + env_file: + - .env + environment: + APP_ENV: ${APP_ENV:-production} + APP_DEBUG: ${APP_DEBUG:-false} + DB_CONNECTION: mysql + DB_HOST: db + DB_PORT: 3306 + DB_DATABASE: ${DB_DATABASE:-nextchair} + DB_USERNAME: ${DB_USERNAME:-nextchair} + DB_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD in .env} + command: ["php", "artisan", "schedule:work"] + depends_on: + db: + condition: service_healthy + volumes: + - nextchair_storage:/var/www/html/storage + +volumes: + nextchair_mysql_data: + nextchair_storage: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..78141b7 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -eu + +cd /var/www/html + +mkdir -p \ + storage/framework/cache/data \ + storage/framework/sessions \ + storage/framework/views \ + storage/logs \ + bootstrap/cache + +chown -R www-data:www-data storage bootstrap/cache +chmod -R ug+rwX storage bootstrap/cache + +# This is safe to run on every start and does not perform migrations. +php artisan storage:link --force >/dev/null 2>&1 || true + +if [ "$(id -u)" = "0" ]; then + exec gosu www-data "$@" +fi + +exec "$@" diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..a564b6e --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,26 @@ +server { + listen 80; + listen [::]:80; + server_name _; + + root /var/www/html/public; + index index.php; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + + location ~ \.php$ { + try_files $uri =404; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /var/www/html/public$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT /var/www/html/public; + fastcgi_pass app:9000; + fastcgi_read_timeout 90s; + } + + location ~ /\. { deny all; } +}