open-productive-stack/drupal/README.md
rnsrk f8b8f53d54 Add Drupal headless stack with Next.js frontend
- Add Next.js frontend service (nextjs) with Dockerfile and source
- Update docker-compose.yml: image names, Drupal 11.3.3, nextjs service
- Add docker-compose.override.yml.disabled for dev hot-reload
- Add install-headless-modules.sh for OAuth/JSON:API module setup
- Add README.md with full setup and configuration guide
- Update nginx/Dockerfile and nginx.conf.template for cms. subdomain
- Update drupal/Dockerfile PHP-FPM build args
- Gitignore **/.vscode/ to prevent IDE workspace files from being tracked
2026-03-30 11:14:17 +02:00

425 lines
13 KiB
Markdown

# 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=<Client ID from the consumer list>
DRUPAL_CLIENT_SECRET=<Secret from the consumer edit form>
DRUPAL_OAUTH_SCOPE=<machine name from step 1, e.g. nextjs_frontend>
```
**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/)