- 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 |
||
|---|---|---|
| .. | ||
| drupal | ||
| nextjs | ||
| nginx | ||
| docker-compose.override.yml.disabled | ||
| docker-compose.yml | ||
| install-headless-modules.sh | ||
| README.md | ||
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)
cd /var/deploy/drupal
./setup-aliases.sh
source ~/.bash-aliases
This creates convenient shortcuts in ~/.bash-aliases:
ddrush- Run Drush commandsdcomposer- Run Composer commandsdphp- Run PHP commandsdshell- Open bash shell in containerdlogs- View PHP-FPM logs (follow mode)dredis- Access Redis CLI
2. Start Services
cd /var/deploy
docker compose -f drupal/docker-compose.yml up -d
3. Install Headless Modules
cd /var/deploy/drupal
./install-headless-modules.sh
4. Enable Core Modules
# 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
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:
$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:
// 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 integrationdrupal/jsonapi_extras- JSON:API enhancementsdrupal/simple_oauth- OAuth 2.0 authenticationdrupal/pathauto- URL aliasesdrupal/cors- CORS headersdrupal/decoupled_router- Route resolution
Common Commands
Shell Aliases (Recommended)
After running ./setup-aliases.sh:
# 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
# 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
# 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
# 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
# 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
- Navigate to
/admin/config/services/next - 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
- The env page (
/admin/config/services/next/sites/{site_id}/env) shows a template;DRUPAL_CLIENT_IDandDRUPAL_CLIENT_SECRETwill 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
- Navigate to
/admin/config/services/cors - 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):
cd /var/deploy/drupal && docker compose -f docker-compose.yml up -d nextjs --force-recreate
Verify OAuth:
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
- Navigate to
/admin/config/services/jsonapi/extras - Configure resource types:
- Disable unwanted resources
- Rename fields for frontend convenience
- Configure includes and sparse fieldsets
JSON:API Endpoints
Examples
# 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
# 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
# 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
# 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
# Check if module is installed
dcomposer show drupal/module_name
# Install if missing
dcomposer require drupal/module_name
Clear all caches
# 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
# 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
- Make changes to code in
drupal/root/ - Clear cache:
ddrush cr(ordocker exec drupal-fpm bash -c "cd /opt/drupal && drush cr") - Export config:
ddrush cex -y(ordocker exec drupal-fpm bash -c "cd /opt/drupal && drush cex -y") - Commit changes to git
- Deploy: Pull changes and run
ddrush cim -yon production
Production Checklist
- Set
opcache.validate_timestamps=0in 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