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
This commit is contained in:
parent
71a8dac389
commit
f8b8f53d54
85 changed files with 7802 additions and 17 deletions
425
drupal/README.md
Normal file
425
drupal/README.md
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
# 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/)
|
||||
Loading…
Add table
Add a link
Reference in a new issue