commit acd91171da006f3c26694679a2eae9ea46bd9c13 Author: Philip Peterson <1326208+philip-peterson@users.noreply.github.com> Date: Mon Apr 20 00:33:56 2026 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cc6e85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Drupal generated files (bind-mounted, not tracked) +web/sites/default/files/* +!web/sites/default/files/.gitkeep +!web/sites/default/files/resume.pdf + +# Never commit real credentials +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..75106f0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +FROM php:8.5-fpm + +RUN apt-get update && apt-get install -y --no-install-recommends \ + nginx \ + supervisor \ + postgresql-client \ + libpq-dev \ + libpng-dev \ + libjpeg-dev \ + libfreetype-dev \ + libzip-dev \ + git \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +RUN docker-php-ext-configure gd --with-freetype --with-jpeg && \ + docker-php-ext-install -j"$(nproc)" \ + pdo_pgsql \ + pgsql \ + gd \ + zip \ + exif \ + bcmath + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +# Copy composer manifest first for layer caching; install pulls Drupal from Packagist. +# To use ../drupal instead, add it as a path repository in composer.json: +# "repositories": [{"type": "path", "url": "../drupal/core", "options": {"symlink": false}}] +# then bump drupal/core-recommended to "12.x-dev@dev" and rebuild. +COPY composer.json ./ +RUN composer install --no-dev --optimize-autoloader --no-interaction + +# Overlay our site-specific files on top of the scaffolded web/ +COPY web/sites/default/settings.php web/sites/default/settings.php +COPY web/sites/default/files/ web/sites/default/files/ +COPY config/sync/ config/sync/ + +# Debian nginx runs as www-data (matches php-fpm), config in conf.d/ +RUN rm -f /etc/nginx/sites-enabled/default +COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf + +COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY docker/php/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +RUN chown -R www-data:www-data web/sites/default/files && \ + chmod -R 755 web/sites/default/files && \ + chmod 444 web/sites/default/settings.php + +EXPOSE 80 +ENTRYPOINT ["/entrypoint.sh"] diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..dcf27b7 --- /dev/null +++ b/composer.json @@ -0,0 +1,40 @@ +{ + "name": "mysite/portfolio", + "description": "Portfolio/resume Drupal site", + "type": "project", + "license": "GPL-2.0-or-later", + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "composer/installers": "^2.3", + "drupal/core-composer-scaffold": "^12", + "drupal/core-recommended": "^12", + "drush/drush": "^14 || dev-main" + }, + "config": { + "allow-plugins": { + "composer/installers": true, + "drupal/core-composer-scaffold": true + }, + "sort-packages": true + }, + "extra": { + "drupal-scaffold": { + "locations": { + "web-root": "web/" + }, + "file-mapping": { + "[web-root]/sites/default/settings.php": false + } + }, + "installer-paths": { + "web/core": ["type:drupal-core"], + "web/modules/contrib/{$name}": ["type:drupal-module"], + "web/profiles/contrib/{$name}": ["type:drupal-profile"], + "web/themes/contrib/{$name}": ["type:drupal-theme"], + "drush/Commands/contrib/{$name}": ["type:drupal-drush"], + "web/modules/custom/{$name}": ["type:drupal-custom-module"], + "web/themes/custom/{$name}": ["type:drupal-custom-theme"] + } + } +} diff --git a/config/sync/.gitkeep b/config/sync/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d70a07a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:80" + environment: + DB_HOST: postgres + DB_NAME: drupal + DB_USER: drupal + DB_PASS: drupal + SITE_NAME: "Portfolio" + ADMIN_PASS: "${ADMIN_PASS:-admin}" + HASH_SALT: "${HASH_SALT:-replace-this-in-production-with-a-long-random-string}" + volumes: + - ./web/sites/default/files:/var/www/html/web/sites/default/files + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + + postgres: + image: postgres:18-alpine + environment: + POSTGRES_DB: drupal + POSTGRES_USER: drupal + POSTGRES_PASSWORD: drupal + volumes: + - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U drupal -d drupal"] + interval: 5s + timeout: 5s + retries: 20 + # No named volume for data = fully ephemeral (recreated from init.sql on every `docker compose up` after `down`) diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..8a2e59f --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,43 @@ +server { + listen 80; + server_name _; + root /var/www/html/web; + index index.php; + + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { allow all; access_log off; log_not_found off; } + + location ~ /\. { deny all; } + location ~* ^/sites/.*/files/.*\.php$ { deny all; } + location ~* ^/sites/.*/private/ { deny all; } + + location / { + try_files $uri /index.php?$query_string; + } + + location ~ '\.php$|^/update\.php' { + fastcgi_split_path_info ^(.+?\.php)(|/.*)$; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param HTTP_PROXY ""; + fastcgi_intercept_errors on; + fastcgi_read_timeout 300; + } + + # True static assets (always on disk, never PHP-generated) + location ~* \.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf)$ { + try_files $uri =404; + expires max; + access_log off; + } + + # CSS/JS may be aggregated on first request via PHP; fall through to index.php if missing + location ~* \.(css|js)$ { + try_files $uri /index.php?$query_string; + expires max; + access_log off; + } +} diff --git a/docker/php/entrypoint.sh b/docker/php/entrypoint.sh new file mode 100644 index 0000000..e2d29ac --- /dev/null +++ b/docker/php/entrypoint.sh @@ -0,0 +1,43 @@ +#!/bin/sh +set -e + +DB_HOST="${DB_HOST:-postgres}" +DB_USER="${DB_USER:-drupal}" +DB_NAME="${DB_NAME:-drupal}" + +echo "[entrypoint] Waiting for PostgreSQL at ${DB_HOST}..." +until pg_isready -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -q; do + sleep 1 +done +echo "[entrypoint] PostgreSQL is ready." + +cd /var/www/html + +DRUSH="vendor/bin/drush --root=/var/www/html/web" + +HAS_TABLES=$($DRUSH sql:query \ + "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='config';" \ + 2>/dev/null || echo "0") + +if [ "$HAS_TABLES" = "1" ]; then + echo "[entrypoint] Database populated, importing configuration..." + $DRUSH config:import -y 2>/dev/null && \ + echo "[entrypoint] Config imported." || \ + echo "[entrypoint] No config to import, continuing." +else + echo "[entrypoint] Fresh database, installing Drupal..." + $DRUSH site:install minimal \ + --site-name="${SITE_NAME:-Portfolio}" \ + --account-name=admin \ + --account-pass="${ADMIN_PASS:-admin}" \ + -y + echo "[entrypoint] Drupal installed." + + if ls /var/www/html/config/sync/*.yml >/dev/null 2>&1; then + echo "[entrypoint] Importing configuration from sync dir..." + $DRUSH config:import -y + fi +fi + +echo "[entrypoint] Starting services..." +exec supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/docker/postgres/init.sql b/docker/postgres/init.sql new file mode 100644 index 0000000..205c573 --- /dev/null +++ b/docker/postgres/init.sql @@ -0,0 +1,10 @@ +-- Drupal PostgreSQL seed +-- +-- On first run this file is empty; Drupal is installed by the entrypoint via drush. +-- After initial setup, replace this file with a full dump: +-- +-- docker compose exec postgres pg_dump --no-owner --no-acl -U drupal drupal \ +-- > docker/postgres/init.sql +-- +-- Commit the dump so every subsequent `docker compose up` (after `down`) starts +-- from a known-good state without re-running the installer. diff --git a/docker/supervisord.conf b/docker/supervisord.conf new file mode 100644 index 0000000..0e8847b --- /dev/null +++ b/docker/supervisord.conf @@ -0,0 +1,26 @@ +[supervisord] +nodaemon=true +user=root +logfile=/dev/stdout +logfile_maxbytes=0 +pidfile=/var/run/supervisord.pid + +[program:php-fpm] +command=/usr/local/sbin/php-fpm -F +autostart=true +autorestart=true +priority=10 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autostart=true +autorestart=true +priority=20 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/web/sites/default/files/.gitkeep b/web/sites/default/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web/sites/default/settings.php b/web/sites/default/settings.php new file mode 100644 index 0000000..912fbb1 --- /dev/null +++ b/web/sites/default/settings.php @@ -0,0 +1,30 @@ + 'pgsql', + 'database' => getenv('DB_NAME') ?: 'drupal', + 'username' => getenv('DB_USER') ?: 'drupal', + 'password' => getenv('DB_PASS') ?: 'drupal', + 'host' => getenv('DB_HOST') ?: 'postgres', + 'port' => '5432', + 'prefix' => '', + 'namespace' => 'Drupal\\pgsql\\Driver\\Database\\pgsql', + 'autoload' => 'core/modules/pgsql/src/Driver/Database/pgsql/', +]; + +// Outside the web root — safe from direct HTTP access. +$settings['config_sync_directory'] = '/var/www/html/config/sync'; + +$settings['hash_salt'] = getenv('HASH_SALT') ?: 'replace-this-in-production'; + +$settings['update_free_access'] = FALSE; + +// Disable CSS/JS aggregation — assets served directly from source paths. +$config['system.performance']['css']['preprocess'] = FALSE; +$config['system.performance']['js']['preprocess'] = FALSE; + +if ($trusted = getenv('TRUSTED_HOST')) { + $settings['trusted_host_patterns'] = ['^' . preg_quote($trusted, '/') . '$']; +} else { + $settings['trusted_host_patterns'] = ['^localhost$', '^127\.0\.0\.1$', '^0\.0\.0\.0$']; +}