# Headless Drupal Stack Drupal 11 backend for Next.js frontend with JSON:API, OAuth authentication, and Redis caching. ## Quick Start ### 1. Setup Shell Aliases (Optional but Recommended) ```bash cd /var/deploy/drupal ./setup-aliases.sh source ~/.bash-aliases ``` This creates convenient shortcuts in `~/.bash-aliases`: - `ddrush` - Run Drush commands - `dcomposer` - Run Composer commands - `dphp` - Run PHP commands - `dshell` - Open bash shell in container - `dlogs` - View PHP-FPM logs (follow mode) - `dredis` - Access Redis CLI ### 2. Start Services ```bash cd /var/deploy docker compose -f drupal/docker-compose.yml up -d ``` ### 3. Install Headless Modules ```bash cd /var/deploy/drupal ./install-headless-modules.sh ``` ### 4. Enable Core Modules ```bash # With alias ddrush en next jsonapi_extras simple_oauth consumers pathauto cors decoupled_router -y # Without alias docker exec drupal-fpm bash -c "cd /opt/drupal && drush en next jsonapi_extras simple_oauth consumers pathauto cors decoupled_router -y" ``` ### 5. Generate OAuth Keys ```bash docker exec drupal-fpm bash -c 'cd /opt/drupal && mkdir -p oauth/keys && openssl genrsa -out oauth/keys/private.key 2048 && openssl rsa -in oauth/keys/private.key -pubout -out oauth/keys/public.key && chown www-data:www-data oauth/keys/private.key oauth/keys/public.key && chmod 640 oauth/keys/private.key oauth/keys/public.key' ``` (PHP-FPM runs as www-data; keys must be readable by it.) Add to `drupal/root/web/sites/default/settings.php`: ```php $settings['simple_oauth.keys'] = [ 'public' => '/opt/drupal/oauth/keys/public.key', 'private' => '/opt/drupal/oauth/keys/private.key', ]; ``` ### 6. Configure Redis Cache Add to `drupal/root/web/sites/default/settings.php`: ```php // Redis Configuration. $settings['redis.connection']['interface'] = 'PhpRedis'; $settings['redis.connection']['host'] = 'drupal-redis'; $settings['redis.connection']['port'] = 6379; $settings['cache']['default'] = 'cache.backend.redis'; $settings['cache']['bins']['bootstrap'] = 'cache.backend.chainedfast'; $settings['cache']['bins']['discovery'] = 'cache.backend.chainedfast'; $settings['cache']['bins']['config'] = 'cache.backend.chainedfast'; ``` ## Stack Components ### Containers - **drupal-reverse-proxy**: NGINX reverse proxy (port 80) - **drupal-fpm**: PHP 8.4 FPM with Drupal 11 - **drupal-redis**: Redis 7 cache backend ### Key Modules - `drupal/next` - Next.js integration - `drupal/jsonapi_extras` - JSON:API enhancements - `drupal/simple_oauth` - OAuth 2.0 authentication - `drupal/pathauto` - URL aliases - `drupal/cors` - CORS headers - `drupal/decoupled_router` - Route resolution ## Common Commands ### Shell Aliases (Recommended) After running `./setup-aliases.sh`: ```bash # Composer commands dcomposer require drupal/module_name dcomposer update dcomposer show # Drush commands ddrush cr # Clear cache ddrush en module_name -y # Enable module ddrush updb -y # Database updates ddrush cex -y # Export configuration ddrush cim -y # Import configuration ddrush status # Site status # PHP commands dphp -v # PHP version dphp -m # PHP modules dphp -i # PHP info # Container access dshell # Open bash shell dlogs # View logs dredis ping # Test Redis ``` ### Without Aliases (Full Commands) #### Composer ```bash # Install module docker exec drupal-fpm bash -c "cd /opt/drupal && composer require drupal/module_name" # Update dependencies docker exec drupal-fpm bash -c "cd /opt/drupal && composer update" # Show installed packages docker exec drupal-fpm bash -c "cd /opt/drupal && composer show" ``` #### Drush ```bash # Clear cache docker exec drupal-fpm bash -c "cd /opt/drupal && drush cr" # Enable module docker exec drupal-fpm bash -c "cd /opt/drupal && drush en module_name -y" # Database updates docker exec drupal-fpm bash -c "cd /opt/drupal && drush updb -y" # Export configuration docker exec drupal-fpm bash -c "cd /opt/drupal && drush cex -y" # Import configuration docker exec drupal-fpm bash -c "cd /opt/drupal && drush cim -y" # Site status docker exec drupal-fpm bash -c "cd /opt/drupal && drush status" ``` ### Container Management ```bash # View logs docker logs drupal-fpm docker logs drupal-reverse-proxy # Access container shell docker exec -it drupal-fpm bash # Restart services docker compose -f drupal/docker-compose.yml restart # Rebuild container docker compose -f drupal/docker-compose.yml up -d --build ``` ### Redis ```bash # With alias dredis ping # Test connection dredis monitor # Monitor Redis dredis keys '*' # Check keys dredis flushall # Clear cache # Without alias docker exec drupal-redis redis-cli ping docker exec drupal-redis redis-cli monitor docker exec drupal-redis redis-cli keys '*' docker exec drupal-redis redis-cli flushall ``` ## Configuration ### Next.js Site Configuration 1. Navigate to `/admin/config/services/next` 2. Add Next.js site: - Label: Production Frontend - Base URL: `https://nasarek.dev` - Preview URL: `https://nasarek.dev/api/preview` - Preview Secret: Generate secure random string 3. The env page (`/admin/config/services/next/sites/{site_id}/env`) shows a template; `DRUPAL_CLIENT_ID` and `DRUPAL_CLIENT_SECRET` will say "Retrieve this from /admin/config/services/consumer" — get the actual Client ID and secret from the OAuth consumer (see OAuth Consumer section below). **Preview secret expired:** If you see "The provided secret has expired", increase the secret lifetime (default is 30 seconds): - **Via UI:** `/admin/config/services/next/settings` → Preview URL generator → Simple OAuth → Secret expiration time → set to 300 (5 min) or 3600 (1 h) - **Via Drush:** `ddrush cset next.settings preview_url_generator_configuration.secret_expiration 300 -y` ### CORS Configuration 1. Navigate to `/admin/config/services/cors` 2. Add configuration: - Enabled: ✓ - Domains: `https://nasarek.dev|*` - Methods: `GET|POST|OPTIONS` - Headers: `Authorization|Content-Type|*` - Credentials: ✓ ### OAuth Consumer **1. Create a dynamic scope** (required; Drupal rejects token requests without a valid scope): - Navigate to `/admin/config/people/simple_oauth/oauth2_scope/dynamic/add` - Role: Authenticated user (or a dedicated role) - Granularity: Role - Grant Types: Enable **Client credentials** - Description: e.g. Next.js Frontend - Machine-readable name: e.g. `nextjs_frontend` - Save **2. Create or edit a consumer:** - Navigate to `/admin/config/services/consumer` (or add at `/admin/config/services/consumer/add`) - Label: e.g. Next.js Frontend (or use existing default_consumer) - **Client ID**: Use the **Client ID** column value from the consumer list (e.g. `lcXmAvaCtkjDhgKsLeAfUucOibVwUxKXtla8b_4CC4w`), **not** the UUID or label - **Secret**: Not shown in the list; edit the consumer to generate or regenerate it - Grant Types: Enable **Client credentials** - Scopes: Select the scope created in step 1 (e.g. `nextjs_frontend`) - Redirect URI: `https://nasarek.dev` - Is 3rd party?: Leave **unchecked** (Next.js is your own frontend) - Save **3. Add to `nextjs/.env.local`:** ``` DRUPAL_CLIENT_ID= DRUPAL_CLIENT_SECRET= DRUPAL_OAUTH_SCOPE= ``` **4. Recreate Next.js container** (env vars are read at container start): ```bash cd /var/deploy/drupal && docker compose -f docker-compose.yml up -d nextjs --force-recreate ``` **Verify OAuth:** ```bash cd /var/deploy/drupal && docker compose -f docker-compose.yml exec nextjs node -e " const b=process.env.NEXT_PUBLIC_DRUPAL_BASE_URL,i=process.env.DRUPAL_CLIENT_ID,s=process.env.DRUPAL_CLIENT_SECRET,scope=process.env.DRUPAL_OAUTH_SCOPE; const body=new URLSearchParams({grant_type:'client_credentials',client_id:i,client_secret:s}); if(scope) body.set('scope',scope); fetch(b+'/oauth/token',{method:'POST',body}).then(r=>r.json()).then(j=>console.log(j.access_token?'OK':JSON.stringify(j))); " ``` ### Next.js Environment Variables Required in `nextjs/.env.local` (loaded via docker-compose `env_file`): | Variable | Required | Description | |----------|----------|-------------| | `NEXT_PUBLIC_DRUPAL_BASE_URL` | Yes | Drupal JSON:API base URL (e.g. `https://cms.nasarek.dev`) | | `DRUPAL_CLIENT_ID` | Yes* | OAuth consumer Client ID (from consumer list, not UUID) | | `DRUPAL_CLIENT_SECRET` | Yes* | OAuth consumer secret from edit form | | `DRUPAL_OAUTH_SCOPE` | Yes* | OAuth scope machine name (e.g. `nextjs_frontend`) | | `DRUPAL_PREVIEW_SECRET` | No | For draft/preview mode webhook | | `DRUPAL_REVALIDATE_SECRET` | No | For on-demand revalidation webhook | \* Without OAuth credentials, JSON:API requests run as anonymous; protected content will return "Access denied". Use `--force-recreate` after changing env vars. ### JSON:API Configuration 1. Navigate to `/admin/config/services/jsonapi/extras` 2. Configure resource types: - Disable unwanted resources - Rename fields for frontend convenience - Configure includes and sparse fieldsets ## JSON:API Endpoints ### Examples ```bash # Get all articles curl https://cms.nasarek.dev/jsonapi/node/article # Get specific article curl https://cms.nasarek.dev/jsonapi/node/article/{uuid} # Filter by status curl https://cms.nasarek.dev/jsonapi/node/article?filter[status]=1 # Include related entities curl https://cms.nasarek.dev/jsonapi/node/article?include=field_image,uid # Sparse fieldsets curl https://cms.nasarek.dev/jsonapi/node/article?fields[node--article]=title,body,created # Sort and paginate curl https://cms.nasarek.dev/jsonapi/node/article?sort=-created&page[limit]=10&page[offset]=0 ``` ### With Authentication ```bash # Get access token (client credentials - used by next-drupal) curl -X POST https://cms.nasarek.dev/oauth/token \ -d "grant_type=client_credentials" \ -d "client_id=YOUR_CLIENT_UUID" \ -d "client_secret=YOUR_SECRET" # Use token curl https://cms.nasarek.dev/jsonapi/node/article \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ## Debugging ### Enable Xdebug Xdebug is pre-configured and enabled: - Mode: debug - Client host: host.docker.internal - Client port: 9000 - IDE key: VSCODE - Log: `/var/log/xdebug.log` ### View Logs ```bash # PHP-FPM logs docker logs drupal-fpm # NGINX logs docker logs drupal-reverse-proxy # Xdebug log docker exec drupal-fpm tail -f /var/log/xdebug.log ``` ### Check PHP Configuration ```bash # PHP version docker exec drupal-fpm php -v # PHP modules docker exec drupal-fpm php -m # PHP info docker exec drupal-fpm php -i # OPcache status docker exec drupal-fpm php -r "echo json_encode(opcache_get_status(), JSON_PRETTY_PRINT);" ``` ## Troubleshooting ### Module not found ```bash # Check if module is installed dcomposer show drupal/module_name # Install if missing dcomposer require drupal/module_name ``` ### Clear all caches ```bash # Drupal cache ddrush cr # Redis cache docker exec drupal-redis redis-cli flushall # OPcache (restart PHP-FPM) docker compose -f drupal/docker-compose.yml restart drupal-fpm ``` ### Permissions issues ```bash # Fix file permissions docker exec drupal-fpm chown -R www-data:www-data /opt/drupal/web/sites/default/files # Settings.php permissions docker exec drupal-fpm chmod 444 /opt/drupal/web/sites/default/settings.php ``` ## File Structure ``` drupal/ ├── docker-compose.yml # Service definitions ├── install-headless-modules.sh # Module installation script ├── drupal/ │ ├── Dockerfile # PHP-FPM image │ └── root/ # Drupal codebase (mounted volume) │ ├── composer.json │ ├── web/ # Document root │ │ ├── core/ │ │ ├── modules/ │ │ │ ├── contrib/ │ │ │ └── custom/ │ │ ├── themes/ │ │ └── sites/ │ └── oauth/ │ └── keys/ # OAuth RSA keys └── nginx/ ├── Dockerfile └── nginx.conf.template # NGINX configuration ``` ## Development Workflow 1. Make changes to code in `drupal/root/` 2. Clear cache: `ddrush cr` (or `docker exec drupal-fpm bash -c "cd /opt/drupal && drush cr"`) 3. Export config: `ddrush cex -y` (or `docker exec drupal-fpm bash -c "cd /opt/drupal && drush cex -y"`) 4. Commit changes to git 5. Deploy: Pull changes and run `ddrush cim -y` on production ## Production Checklist - [ ] Set `opcache.validate_timestamps=0` in production - [ ] Configure Redis cache - [ ] Generate OAuth keys - [ ] Configure CORS restrictively - [ ] Set up content revalidation webhooks - [ ] Configure rate limiting - [ ] Enable HTTPS only - [ ] Set secure session cookies - [ ] Configure backup strategy - [ ] Monitor logs and performance - [ ] Test JSON:API endpoints with different user roles ## Resources - [Next.js for Drupal](https://next-drupal.org/) - [JSON:API Documentation](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module) - [Simple OAuth](https://www.drupal.org/project/simple_oauth) - [Drupal.org](https://www.drupal.org/)