Production-ready Drupal 11 image, hardened theo Docker Scout Health Score (7/7 policy pass).
| Drupal | 11.3.9 |
| PHP | 8.4.21 (FPM, Alpine 3.23) |
| Drush | 13.3.5 |
| Composer | 2.8.4 |
| PECL | redis 6.1.0, apcu 5.1.24 |
| Architecture | linux/amd64, linux/arm64 |
| License | GPL-2.0-or-later |
| Vendor | ABS Technology |
abstechnology/drupal-nginx — Nginx vhost cho image này (bắt buộc đi kèm).abstechnology/drupal:11.3.9-php8.4.21-fpm-r1 ← immutable, dùng cho production
abstechnology/drupal:11.3.9 ← Drupal patch, auto-bump nginx/PHP minor
abstechnology/drupal:latest ← chỉ nên dùng để thử
mkdir my-drupal && cd my-drupal
Tạo file compose.yaml:
name: drupal-prod
x-logging: &default-logging
driver: json-file
options: { max-size: "10m", max-file: "5" }
x-restart: &default-restart
restart: unless-stopped
x-security: &default-security
security_opt: [no-new-privileges:true]
services:
data-init:
image: alpine:3.20
user: "0:0"
command:
- sh
- -c
- |
set -e
for d in postgres redis drupal/files drupal/private drupal/config backups; do
mkdir -p "/data/$$d"
done
chown -R 70:70 /data/postgres /data/backups
chown -R 999:1000 /data/redis
chown -R 82:82 /data/drupal
volumes:
- ${DATA_DIR:-./data}:/data
restart: "no"
drupal:
image: ${DRUPAL_IMAGE}
pull_policy: missing
<<: [*default-restart, *default-security]
logging: *default-logging
user: "www-data:www-data"
cap_drop: [ALL]
cap_add: [CHOWN, SETUID, SETGID, DAC_OVERRIDE]
depends_on:
data-init: { condition: service_completed_successfully }
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
environment:
DRUPAL_DB_HOST: postgres
DRUPAL_DB_PORT: 5432
DRUPAL_DB_NAME: ${POSTGRES_DB}
DRUPAL_DB_USER: ${POSTGRES_USER}
DRUPAL_DB_PASSWORD: ${POSTGRES_PASSWORD}
DRUPAL_DB_DRIVER: pgsql
DRUPAL_HASH_SALT: ${DRUPAL_HASH_SALT}
DRUPAL_TRUSTED_HOST_PATTERNS: ${DRUPAL_TRUSTED_HOST_PATTERNS}
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
PHP_MEMORY_LIMIT: 512M
volumes:
- ${DATA_DIR:-./data}/drupal/files:/var/www/html/web/sites/default/files
- ${DATA_DIR:-./data}/drupal/private:/var/www/private
- ${DATA_DIR:-./data}/drupal/config:/var/www/config
- drupal_code:/shared
networks: [backend]
healthcheck:
test: ["CMD-SHELL", "php-fpm -t || exit 1"]
interval: 30s
timeout: 5s
retries: 3
drupal-init:
image: ${DRUPAL_IMAGE}
pull_policy: missing
user: "www-data:www-data"
depends_on:
drupal: { condition: service_started }
entrypoint: ["/bin/sh", "-c"]
command: >
"rsync -a --delete --exclude 'web/sites/default/files' /var/www/html/ /shared/"
volumes:
- drupal_code:/shared
networks: [backend]
restart: "no"
nginx:
image: ${NGINX_IMAGE}
pull_policy: missing
<<: [*default-restart, *default-security]
logging: *default-logging
cap_drop: [ALL]
cap_add: [CHOWN, SETUID, SETGID, NET_BIND_SERVICE, DAC_OVERRIDE]
depends_on:
drupal-init: { condition: service_completed_successfully }
ports: ["${HTTP_PORT}:80"]
volumes:
- drupal_code:/var/www/html:ro
- ${DATA_DIR:-./data}/drupal/files:/var/www/html/web/sites/default/files:ro
networks: [backend, frontend]
postgres:
image: postgres:16.6-alpine3.20
<<: [*default-restart, *default-security]
logging: *default-logging
cap_drop: [ALL]
cap_add: [CHOWN, SETUID, SETGID, DAC_OVERRIDE, FOWNER, FSETID]
depends_on:
data-init: { condition: service_completed_successfully }
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- ${DATA_DIR:-./data}/postgres:/var/lib/postgresql/data
- ${DATA_DIR:-./data}/backups:/backups
networks: [backend]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
retries: 5
redis:
image: redis:7.4.1-alpine3.20
<<: [*default-restart, *default-security]
logging: *default-logging
user: "redis:redis"
cap_drop: [ALL]
cap_add: [SETUID, SETGID]
depends_on:
data-init: { condition: service_completed_successfully }
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}",
"--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru",
"--appendonly", "yes"]
volumes:
- ${DATA_DIR:-./data}/redis:/data
networks: [backend]
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
retries: 5
networks:
frontend: { driver: bridge }
backend: { driver: bridge }
volumes:
drupal_code:
Tạo file .env:
# Image versions (lấy tag từ Docker Hub)
DRUPAL_IMAGE=abstechnology/drupal:11.3.9-php8.4.21-fpm-r1
NGINX_IMAGE=abstechnology/drupal-nginx:11.3.9-r1
# Public HTTP port
HTTP_PORT=8080
# Persistent data root (host folder)
DATA_DIR=./data
# Database
POSTGRES_DB=drupal
POSTGRES_USER=drupal
POSTGRES_PASSWORD='CHANGE_ME_strong_db_password'
# Redis
REDIS_PASSWORD='CHANGE_ME_strong_redis_password'
# Drupal (sinh hash bằng: openssl rand -base64 64 | tr -d '\n')
DRUPAL_HASH_SALT='CHANGE_ME_generate_with_openssl_rand_base64_64'
DRUPAL_TRUSTED_HOST_PATTERNS='^localhost$|^127\.0\.0\.1$'
DRUPAL_SITE_NAME='My Drupal Site'
# Admin user - chỉ dùng cho lần install đầu, sau đó đổi qua UI / drush
DRUPAL_ADMIN_NAME=admin
DRUPAL_ADMIN_PASS='CHANGE_ME_admin_password'
[email protected]
BẮT BUỘC đổi 3 password trên trước khi up. Sinh hash salt:
openssl rand -base64 64 | tr -d '\n' && echo
docker compose up -d
Service drupal-install sẽ tự chạy drush site:install lần đầu (với credentials trong .env), sau đó enable module Redis. Lần sau up sẽ phát hiện DB đã có schema và skip install (idempotent).
Theo dõi tiến trình install (mất ~30s lần đầu):
docker compose logs -f drupal-install
# → "✓ Drupal cài xong" khi xong
URL: http://localhost:${HTTP_PORT}
User: ${DRUPAL_ADMIN_NAME} ← lấy từ .env
Pass: ${DRUPAL_ADMIN_PASS} ← lấy từ .env
BẮT BUỘC: sau khi đăng nhập lần đầu, đổi password qua
/user/1/edithoặc:docker compose exec -u www-data drupal drush user:password "${DRUPAL_ADMIN_NAME}" "newSecurePass!"Sau đó có thể xóa
DRUPAL_ADMIN_PASSkhỏi.envđể đỡ rủi ro lộ.
# Xem logs
docker compose logs -f drupal
# Drush
docker compose exec -u www-data drupal drush status
docker compose exec -u www-data drupal drush cr # clear cache
docker compose exec -u www-data drupal drush en redis -y # bật module Redis
# Composer (cài contrib module)
docker compose exec -u www-data drupal composer require drupal/webform
# Backup DB
docker compose exec -T postgres pg_dump -U drupal -F c drupal \
> backups/$(date +%Y%m%d-%H%M%S).dump
# Restore DB
cat backup.dump | docker compose exec -T postgres pg_restore \
-U drupal -d drupal --clean --if-exists
# Stop / start
docker compose stop
docker compose start
docker compose down # giữ data
docker compose down -v # XÓA cả named volume drupal_code
Browser
│ HTTP :${HTTP_PORT}
▼
┌──────────────┐ FastCGI :9000 ┌────────────────┐ :5432 ┌──────────┐
│ drupal-nginx │ ─────────────────►│ drupal │ ────────► │ postgres │
│ (web server) │ ◄─response────── │ (PHP-FPM) │ └──────────┘
└──────────────┘ │ Drupal 11.3.9 │ :6379 ┌──────────┐
└────────────────┘ ────────► │ redis │
└──────────┘
Init containers (chạy 1 lần khi up, exit 0):
data-init → mkdir + chown ./data/{postgres,redis,drupal,backups}
drupal-install → drush site:install + drush en redis (chỉ lần đầu)
drupal-init → rsync code Drupal sang named volume cho nginx mount
Tất cả data nằm trong ${DATA_DIR} (default ./data/):
data/
├── postgres/ ← DB files (uid 70)
├── redis/ ← AOF persistence (uid 999)
├── drupal/
│ ├── files/ ← public uploads (uid 82, www-data)
│ ├── private/ ← private files
│ └── config/ ← drush cex/cim
└── backups/ ← pg_dump output
Backup folder data/ là backup toàn bộ stack.
www-data (uid 82)cap_drop: ALL + cap_add minimalno-new-privileges:truedocker scout cves abstechnology/drupal:latestgithub.com/abstechnology/drupal — Dockerfile, build scripts, CI.
Content type
Image
Digest
sha256:a70e022fb…
Size
207 MB
Last updated
3 months ago
docker pull abstechnology/drupal