Проблема с разрешением файла/каталога WordPress + Rootless Docker [закрыто]Php

Кемеровские программисты php общаются здесь
Ответить
Anonymous
 Проблема с разрешением файла/каталога WordPress + Rootless Docker [закрыто]

Сообщение Anonymous »

Я запускаю сайт WordPress (https://example.com) за Traefik на VPS с использованием Docker без root (сокет демона Docker — /run/user/1000/docker.sock).
Все работает нормально, за исключением обновления плагинов из wp-admin. Любое обновление плагина (а не только одного конкретного плагина) завершается с ошибкой, например:

Обновление плагина ACF Content Analysis для Yoast SEO (1/24) Загрузка
обновления с
https://downloads.wordpress.org/plugin/ ... eo.3.2.zip
Произошла ошибка при обновлении ACF Content Analysis для Yoast. SEO:
Загрузка не удалась. Каталог назначения для потоковой передачи файлов
не существует или недоступен для записи.

Поздние попытки приводили к следующим вариантам:

PCLZIP_ERR_MISSING_FILE (-4): Отсутствует архивный файл
'/var/www/html/wp-content/upgrade/…zip'
Не удалось создать каталог...
Не удалось создать каталог обновления-временного резервного копирования

Наконец: обновление невозможно установить, поскольку некоторые файлы не удалось скопировать. Обычно это происходит из-за несогласованных прав доступа к файлам.
По сути: все обновления плагинов завершаются с ошибкой на разных этапах файловой системы (загрузка, распаковка, резервное копирование или копирование).
Среда
  • Ubuntu VPS
  • Docker без root (запускается от имени пользователя без полномочий root, сокет
  • /run/user/1000/docker.sock)
  • Обратный прокси-сервер Traefik + Cloudflare DNS-01
  • WordPress wordpress:php8.3-fpm за nginx:alpine
  • MariaDB 11.8
  • Привязка кодовой базы с хоста: ./html:/var/www/html
Составление Traefik (соответствующие части):

Код: Выделить всё

services:
traefik:
image: traefik:latest
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
command:
- --global.checkNewVersion=true
- --global.sendAnonymousUsage=false

# Providers
- --providers.docker=true
- --providers.docker.endpoint=unix:///var/run/docker.sock
- --providers.docker.exposedByDefault=false
- --providers.docker.network=web
- --providers.file.directory=/dynamic
- --providers.file.watch=true

# Entrypoints
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
- --entrypoints.websecure.address=:443
- --entrypoints.websecure.http.tls=true
- --entrypoints.websecure.http.tls.certResolver=cfdns

# TLS Options
- --entrypoints.websecure.http.tls.options=default

# API
- --api.insecure=true
- --api.dashboard=true

# Logging
- --log.level=INFO
- --accessLog=true

# ACME via Cloudflare DNS
- --certificatesResolvers.cfdns.acme.email=contact@corp.com
- --certificatesResolvers.cfdns.acme.storage=/letsencrypt/acme.json
- --certificatesResolvers.cfdns.acme.keyType=EC256
- --certificatesResolvers.cfdns.acme.dnsChallenge.delayBeforeCheck=30
- --certificatesResolvers.cfdns.acme.dnsChallenge.resolvers=1.1.1.1:53,8.8.8.8:53
- --certificatesResolvers.cfdns.acme.dnsChallenge.provider=cloudflare

- --ping=true

ports:
- "80:80"
- "443:443"
- "127.0.0.1:8080:8080"

environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}

volumes:
- /run/user/1000/docker.sock:/var/run/docker.sock:ro
- ./dynamic:/dynamic:ro
- ./letsencrypt:/letsencrypt
- ./logs:/var/log/traefik

networks:
- web

networks:
web:
external: true
Динамическая конфигурация Traefik (заголовки безопасности + параметры TLS):

Код: Выделить всё

http:
middlewares:
security-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
contentTypeNosniff: true
referrerPolicy: no-referrer-when-downgrade
browserXssFilter: true
permissionsPolicy: >-
geolocation=(), microphone=(), camera=(), fullscreen=(self)
gzip:
compress: {}
rate-limit:
rateLimit:
average: 100
burst: 50

tls:
options:
default:
minVersion: VersionTLS12
cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
sniStrict: true
curvePreferences:
- CurveP521
- CurveP384
Стек WordPress (пример):

Код: Выделить всё

networks:
web:            # external, only for Traefik   nginx
external: true
example_net:    # internal, unique to this stack
external: false

volumes:
redisdata:
nginx_cache: {}

services:
db:
image: mariadb:11.8
restart: unless-stopped
environment:
MARIADB_DATABASE: ${MARIADB_DATABASE}
MARIADB_USER: ${MARIADB_USER}
MARIADB_PASSWORD: ${MARIADB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
command: ["--transaction-isolation=READ-COMMITTED", "--binlog_expire_logs_seconds=2592000", "--innodb_buffer_pool_size=512M"]
volumes:
- ./db:/var/lib/mysql
networks: [example_net]
healthcheck:
test: ["CMD-SHELL", "mariadb-admin ping -uroot -p${MARIADB_ROOT_PASSWORD} --silent"]
interval: 10s
timeout: 5s
retries: 5

wordpress:
image: wordpress:php8.3-fpm
restart: unless-stopped
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: ${MARIADB_DATABASE}
WORDPRESS_DB_USER: ${MARIADB_USER}
WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD}
WP_REDIS_HOST: redis
WP_REDIS_PORT: 6379
volumes:
- ./html:/var/www/html       # bind mount from host
depends_on:
db:
condition: service_healthy
networks: [example_net]

nginx:
image: nginx:alpine
restart: unless-stopped
depends_on:
- wordpress
volumes:
- ./html:/var/www/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./cache:/var/cache/nginx
- nginx_cache:/var/cache/nginx
networks: [web, example_net]
labels:
- "traefik.enable=true"
- "traefik.docker.network=web"

# HTTP → HTTPS redirect
- "traefik.http.middlewares.example-redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.example-redirect.redirectscheme.permanent=true"

- "traefik.http.routers.example-https.middlewares=security-headers@file,gzip@file,rate-limit@file"

# HTTP router
- "traefik.http.routers.example-http.rule=Host(`example.com`) || Host(`www.example.com`)"
- "traefik.http.routers.example-http.entrypoints=web"
- "traefik.http.routers.example-http.middlewares=example-redirect"
- "traefik.http.routers.example-http.service=example"

# HTTPS router
- "traefik.http.routers.example-https.rule=Host(`example.com`) || Host(`www.example.com`)"
- "traefik.http.routers.example-https.entrypoints=websecure"
- "traefik.http.routers.example-https.tls=true"
- "traefik.http.routers.example-https.tls.certresolver=cfdns"
- "traefik.http.routers.example-https.service=example"

# Explicit service (points at Nginx in the container)
- "traefik.http.services.example.loadbalancer.server.port=80"

redis:
image: redis:7.4-alpine
restart: unless-stopped
command: ["redis-server", "--save", "", "--appendonly", "no"]
volumes:
- redisdata:/data
networks: [example_net]
Виртуальный хост Nginx:

Код: Выделить всё

fastcgi_cache_key "$scheme$request_method$host$request_uri";

server {
listen 80;
server_name _;

root /var/www/html;
index index.php index.html;

# Skip cache for logged-in, POST, search, preview, feeds
set $skip_cache 0;
if ($request_method = POST)         { set $skip_cache 1; }
if ($query_string != "")            { set $skip_cache 1; }
if ($http_cookie ~* "comment_author|wordpress_logged_in|wp-postpass") { set $skip_cache 1; }
if ($request_uri ~* "/wp-admin/|/wp-login.php|/xmlrpc.php|/feed|/preview=") { set $skip_cache 1;  }

location / {
try_files $uri $uri/ /index.php?$args;
}

location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass wordpress:9000;

fastcgi_cache WPFC;
fastcgi_cache_valid 200 301 302 10m;
fastcgi_cache_min_uses 2;
add_header X-FastCGI-Cache $upstream_cache_status;

fastcgi_no_cache $skip_cache;
fastcgi_cache_bypass $skip_cache;

fastcgi_read_timeout 120s;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}

location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|webp|avif)$ {
expires 30d;
access_log off;
}

client_max_body_size 64m;
}
То, что я уже пробовал
На хосте (как пользователь развертывания, запускающий Docker без root-доступа), я пробовал:
Изменить владельца папки, смонтированной привязкой:

Код: Выделить всё

sudo chown -R 33:33 ./html        # www-data
sudo chown -R deploy:deploy ./html
Настройка разрешений:

Код: Выделить всё

find html -type d -exec chmod 755 {} \;
find html -type f -exec chmod 644 {} \;

chmod 777 html/wp-content
chmod -R 777 html/wp-content/plugins
chmod -R 777 html/wp-content/upgrade
chmod -R 777 html/wp-content/upgrade-temp-backup
chmod -R 777 html/wp-content/uploads
Исправление wp-config.php, чтобы обеспечить:

Код: Выделить всё

define('FS_METHOD', 'direct');
устанавливается перед require_once ABSPATH . 'wp-settings.php';
Проверка WP_TEMP_DIR (удалены все старые значения с предыдущего хостинга).
Даже после этих изменений любое обновление плагина по-прежнему завершается с ошибкой, часто с:

Обновление не может быть установлено, поскольку некоторые файлы не удалось скопировать.
Обычно это происходит из-за несогласованных прав доступа к файлам.
/>
Значит, это влияет на все плагины, а не только на ACF/Yoast.
Вопрос
Учитывая, что это:
  • WordPress в WordPress: PHP8.3-fpm
  • За Nginx и Traefik
  • Работа в безрутовых системах Docker
  • С файлами WP, привязанными к хосту (./html:/var/www/html).
Каков правильный, безопасный и постоянный способ настройки владения файлами/разрешениями (или томами), чтобы:
обновления плагина/темы/ядра WordPress от wp-admin надежно работали для всех плагинов, и
я не нужно каждый раз вводить chmod 777 или каждый раз вручную удалять/переустанавливать плагины?
В частности, существует ли рекомендуемый шаблон для WordPress + Docker без root (именованные тома или привязки, какой пользователь должен владеть /var/www/html и т. д.), который вообще позволяет избежать этих ошибок «Каталог назначения не доступен для записи / противоречивые разрешения»?

Подробнее здесь: https://stackoverflow.com/questions/798 ... sion-issue
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Php»