This commit is contained in:
latypov committed 2026-09-29 22:08:18 +07:00
commit 6765ec8991
17 files changed
+825

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
.git
.gitignore
.env
backup
secrets
data
*.zip
+21
View File
@@ -0,0 +1,21 @@
COMPOSE_PROJECT_NAME=egeya
EGEYA_VERSION=4199
EGEYA_URL=
EGEYA_SHA256=
EGEYA_DB_NAME=egeya
EGEYA_DB_USER=egeya
MARIADB_VERSION=10.11
HTTP_BIND=127.0.0.1
HTTP_PORT=8080
NGINX_SERVER_NAME=_
PHP_MEMORY_LIMIT=512M
PHP_UPLOAD_MAX_FILESIZE=512M
PHP_POST_MAX_SIZE=1024M
PHP_MAX_EXECUTION_TIME=300
PHP_MAX_INPUT_TIME=300
TZ=Europe/Berlin
+12
View File
@@ -0,0 +1,12 @@
.env
backup/*
!backup/.gitkeep
data/*
!data/user/
!data/user/.gitkeep
!data/pictures/
!data/pictures/.gitkeep
!data/tmp/
!data/tmp/.gitkeep
secrets/*
!secrets/.gitkeep
+127
View File
@@ -0,0 +1,127 @@
ARG EGEYA_VERSION=4199
ARG EGEYA_URL=""
ARG EGEYA_SHA256=""
FROM debian:bookworm-slim AS egeya-source
ARG EGEYA_VERSION
ARG EGEYA_URL
ARG EGEYA_SHA256
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates curl unzip; \
rm -rf /var/lib/apt/lists/*; \
archive_url="${EGEYA_URL:-https://blogengine.ru/download/e2_distr_v${EGEYA_VERSION}.zip}"; \
work_dir="$(mktemp -d)"; \
curl -fL \
--retry 5 \
--retry-delay 2 \
--connect-timeout 20 \
--user-agent "egeya-docker/${EGEYA_VERSION}" \
"$archive_url" \
-o "$work_dir/egeya.zip"; \
if [ -n "$EGEYA_SHA256" ]; then \
echo "$EGEYA_SHA256 $work_dir/egeya.zip" | sha256sum -c -; \
fi; \
mkdir -p "$work_dir/unpacked" /opt/egeya-release; \
unzip -q "$work_dir/egeya.zip" -d "$work_dir/unpacked"; \
if [ -f "$work_dir/unpacked/index.php" ]; then \
source_dir="$work_dir/unpacked"; \
else \
source_dir="$(find "$work_dir/unpacked" -maxdepth 2 -type f -name index.php -printf '%h\n' | head -n 1)"; \
fi; \
test -n "$source_dir"; \
cp -a "$source_dir"/. /opt/egeya-release/; \
test -f /opt/egeya-release/index.php; \
rm -rf "$work_dir"
FROM php:8.2-fpm-bookworm AS php
ENV EGEYA_ROOT=/var/www/html \
EGEYA_DATA_DIR=/var/www/egeya-data \
EGEYA_DEFAULTS_DIR=/opt/egeya-defaults
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
libavif-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
libwebp-dev \
libzip-dev; \
docker-php-ext-configure gd \
--with-avif \
--with-freetype \
--with-jpeg \
--with-webp; \
docker-php-ext-install -j"$(nproc)" \
exif \
gd \
mbstring \
opcache \
pdo_mysql \
zip; \
mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"; \
rm -rf /var/lib/apt/lists/*
COPY --from=egeya-source /opt/egeya-release/ /var/www/html/
RUN set -eux; \
mkdir -p "$EGEYA_DEFAULTS_DIR/user" "$EGEYA_DEFAULTS_DIR/pictures"; \
if [ -d "$EGEYA_ROOT/user" ]; then \
cp -a "$EGEYA_ROOT/user"/. "$EGEYA_DEFAULTS_DIR/user"/; \
fi; \
if [ -d "$EGEYA_ROOT/pictures" ]; then \
cp -a "$EGEYA_ROOT/pictures"/. "$EGEYA_DEFAULTS_DIR/pictures"/; \
fi; \
rm -rf "$EGEYA_ROOT/user" "$EGEYA_ROOT/pictures"; \
ln -s "$EGEYA_DATA_DIR/user" "$EGEYA_ROOT/user"; \
ln -s "$EGEYA_DATA_DIR/pictures" "$EGEYA_ROOT/pictures"; \
chown -R www-data:www-data "$EGEYA_ROOT" "$EGEYA_DEFAULTS_DIR"
COPY docker/php/egeya-entrypoint.sh /usr/local/bin/egeya-entrypoint
COPY docker/php/www-egeya.conf /usr/local/etc/php-fpm.d/zz-egeya.conf
RUN chmod 0755 /usr/local/bin/egeya-entrypoint
WORKDIR /var/www/html
EXPOSE 9000
HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=5 \
CMD php -r '$s=@fsockopen("127.0.0.1",9000,$e,$m,2); if(!$s){exit(1);} fclose($s);'
ENTRYPOINT ["egeya-entrypoint"]
CMD ["php-fpm"]
FROM nginx:1.28-bookworm AS nginx
ENV EGEYA_ROOT=/var/www/html \
EGEYA_DATA_DIR=/var/www/egeya-data
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends curl; \
rm -rf /var/lib/apt/lists/*; \
if getent passwd www-data >/dev/null; then \
sed -ri 's/^user[[:space:]]+[^;]+;/user www-data;/' /etc/nginx/nginx.conf; \
fi
COPY --from=egeya-source /opt/egeya-release/ /var/www/html/
RUN set -eux; \
rm -rf "$EGEYA_ROOT/user" "$EGEYA_ROOT/pictures"; \
ln -s "$EGEYA_DATA_DIR/user" "$EGEYA_ROOT/user"; \
ln -s "$EGEYA_DATA_DIR/pictures" "$EGEYA_ROOT/pictures"; \
rm -f /etc/nginx/conf.d/default.conf
COPY docker/nginx/egeya.conf.template /etc/nginx/templates/egeya.conf.template
WORKDIR /var/www/html
EXPOSE 80
HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=5 \
CMD curl -fsS http://127.0.0.1/ >/dev/null || exit 1
+316
View File
@@ -0,0 +1,316 @@
# Egeya 11.5 v4199 in Docker
Stack:
- nginx
- PHP-FPM 8.2
- MariaDB 10.11
- Egeya v4199
The layout intentionally follows a tested nginx + PHP-FPM deployment rather than the older Apache-based Docker example.
## Why nginx + PHP-FPM
Egeya 11 supports PHP up to 8.2. The nginx configuration uses synthetic URLs through:
```nginx
location / {
log_not_found off;
rewrite ^(.*)$ /index.php?go=$1 last;
}
```
Do not replace this with:
```nginx
try_files $uri $uri/ /index.php?go=$uri;
```
The rewrite form preserves the original query string used by Egeya upload requests, including fields such as `entity` and `entity-id`.
The PHP location also passes:
```nginx
fastcgi_param E2_SYNTH_URLS true;
```
Direct access to PHP files under `/system` and `/user`, `user/settings.json`, `*.psa`, and `.ht*` is blocked.
## Upload temporary directory
The `data` directory is mounted once at `/var/www/egeya-data`.
Persistent paths are:
```text
/var/www/egeya-data/user
/var/www/egeya-data/pictures
/var/www/egeya-data/tmp
```
Inside both application images, `/var/www/html/user` and `/var/www/html/pictures` are symbolic links into this single mount.
PHP is configured with:
```ini
upload_tmp_dir = /var/www/egeya-data/tmp
sys_temp_dir = /var/www/egeya-data/tmp
```
This is deliberate. A standard container `/tmp` is on the container filesystem, while `pictures` is on a bind mount. Moving an uploaded file between them may cross mount boundaries and fail with `EXDEV`. Keeping the upload temporary directory and `pictures` under the same mount avoids that class of failure.
## Project structure
```text
Dockerfile
compose.yaml
.env.example
.dockerignore
.gitignore
docker/nginx/egeya.conf.template
docker/php/egeya-entrypoint.sh
docker/php/www-egeya.conf
scripts/prepare.sh
scripts/migrate-files.sh
scripts/import-db.sh
data/user
data/pictures
data/tmp
backup
secrets
```
Create the structure manually if required:
```bash
mkdir -p docker/nginx docker/php scripts data/user data/pictures data/tmp backup secrets
touch Dockerfile compose.yaml .env.example .dockerignore .gitignore \
docker/nginx/egeya.conf.template \
docker/php/egeya-entrypoint.sh docker/php/www-egeya.conf \
scripts/prepare.sh scripts/migrate-files.sh scripts/import-db.sh \
data/user/.gitkeep data/pictures/.gitkeep data/tmp/.gitkeep \
backup/.gitkeep secrets/.gitkeep
```
## Prepare
```bash
chmod +x scripts/*.sh
./scripts/prepare.sh
```
Review `.env` before starting.
## Build
```bash
docker compose build --pull
```
Both `nginx` and `php` targets are built from the same Egeya source stage, so the static files seen by nginx and the PHP code executed by PHP-FPM are from the same Egeya release.
Optional source integrity check:
```text
EGEYA_SHA256=<sha256 of e2_distr_v4199.zip>
```
## Start
```bash
docker compose up -d
docker compose ps
docker compose logs --tail=100 nginx php db
```
By default nginx listens only on:
```text
127.0.0.1:8080
```
This is suitable when another reverse proxy terminates HTTPS on the Docker host.
## Data model
The application code is immutable and comes from the Docker images.
Persistent data is limited to:
- `data/user`
- `data/pictures`
- `data/tmp`
- MariaDB named volume `db_data`
Do not copy an old `/system` directory into the container. Updating the image updates `/system` and the bundled application files.
Custom additions stored below `user`, including `user/extras`, migrate together with the user data.
## Migration from a bare-metal installation
Assume the current installation is:
```text
/var/www/blog
```
### 1. Stop writes on the old installation
Stop the web application or otherwise prevent writes for the final synchronization.
### 2. Create a database dump
Example:
```bash
mysqldump \
--single-transaction \
--quick \
--triggers \
--default-character-set=utf8mb4 \
egeya > egeya.sql
```
Copy the dump to:
```text
backup/egeya.sql
```
### 3. Copy file data
If the old blog tree is locally available:
```bash
./scripts/migrate-files.sh /var/www/blog ./data
```
For a remote source, an equivalent rsync is:
```bash
rsync -aHAX old-host:/var/www/blog/user/ ./data/user/
rsync -aHAX old-host:/var/www/blog/pictures/ ./data/pictures/
```
Do not copy `/system`.
Do not copy the old nginx configuration into the application image. Use `docker/nginx/egeya.conf.template`.
### 4. Prepare secrets and configuration
```bash
./scripts/prepare.sh
```
Set the target database name and user in `.env` before initializing MariaDB.
### 5. Start MariaDB and import the dump
```bash
./scripts/import-db.sh backup/egeya.sql
```
### 6. Start PHP and nginx
```bash
docker compose up -d php nginx
```
### 7. Validate
```bash
docker compose ps
docker compose logs --tail=200 php nginx
```
Check at least:
- public pages
- author login
- note editor
- image upload
- audio/video upload if used
- editing an existing note
- built-in security check
- access to `/system/core.php` returns 403
- access to `/user/settings.json` returns 403
For upload diagnostics, the application log is available at:
```text
data/user/logs/main.log
```
It is created by the PHP container entrypoint if absent.
## Database credentials
Egeya receives database settings through its supported environment variables:
```text
E2_DB_SERVER
E2_DB_USER_NAME
E2_DB_PASSW
E2_DB_NAME
```
The Compose file does not place the password directly in its environment section. It mounts a Docker secret and the PHP entrypoint exports its contents as `E2_DB_PASSW` immediately before starting PHP-FPM.
PHP-FPM is explicitly configured with `clear_env = no` so the Egeya process can see these variables.
## Updating Egeya
Change:
```text
EGEYA_VERSION=<new build number>
```
If necessary also set `EGEYA_URL` and `EGEYA_SHA256`.
Then rebuild and recreate the application containers:
```bash
docker compose build --pull php nginx
docker compose up -d --no-deps --force-recreate php nginx
```
The persistent `user`, `pictures`, and database data are not replaced.
Before an update, make a database dump and a copy of `data`.
## Useful commands
Validate Compose rendering:
```bash
docker compose config
```
PHP configuration:
```bash
docker compose exec php php -i | grep -E 'upload_tmp_dir|sys_temp_dir|upload_max_filesize|post_max_size'
```
Verify that temp and pictures resolve to the same mounted filesystem:
```bash
docker compose exec php sh -c 'df -T /var/www/egeya-data/tmp /var/www/egeya-data/pictures'
```
Check the nginx configuration:
```bash
docker compose exec nginx nginx -T
```
Check protected paths:
```bash
curl -i http://127.0.0.1:8080/system/core.php
curl -i http://127.0.0.1:8080/user/settings.json
```
View File
Whitespace-only changes.
+131
View File
@@ -0,0 +1,131 @@
name: ${COMPOSE_PROJECT_NAME:-egeya}
services:
db:
image: mariadb:${MARIADB_VERSION:-10.11}
restart: unless-stopped
environment:
MARIADB_DATABASE: ${EGEYA_DB_NAME:-egeya}
MARIADB_USER: ${EGEYA_DB_USER:-egeya}
MARIADB_PASSWORD_FILE: /run/secrets/db_password
MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
TZ: ${TZ:-UTC}
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
- db_data:/var/lib/mysql
secrets:
- db_password
- db_root_password
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
networks:
- backend
php:
build:
context: .
target: php
args:
EGEYA_VERSION: ${EGEYA_VERSION:-4199}
EGEYA_URL: ${EGEYA_URL:-}
EGEYA_SHA256: ${EGEYA_SHA256:-}
image: ${EGEYA_PHP_IMAGE:-local/egeya-php}:${EGEYA_VERSION:-4199}
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
E2_DB_SERVER: db
E2_DB_USER_NAME: ${EGEYA_DB_USER:-egeya}
E2_DB_PASSW_FILE: /run/secrets/db_password
E2_DB_NAME: ${EGEYA_DB_NAME:-egeya}
PHP_MEMORY_LIMIT: ${PHP_MEMORY_LIMIT:-512M}
PHP_UPLOAD_MAX_FILESIZE: ${PHP_UPLOAD_MAX_FILESIZE:-512M}
PHP_POST_MAX_SIZE: ${PHP_POST_MAX_SIZE:-1024M}
PHP_MAX_EXECUTION_TIME: ${PHP_MAX_EXECUTION_TIME:-300}
PHP_MAX_INPUT_TIME: ${PHP_MAX_INPUT_TIME:-300}
TZ: ${TZ:-UTC}
volumes:
- ./data:/var/www/egeya-data
secrets:
- db_password
healthcheck:
test:
- CMD
- php
- -r
- '$$s=@fsockopen("127.0.0.1",9000,$$e,$$m,2); if(!$$s){exit(1);} fclose($$s);'
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
networks:
- backend
nginx:
build:
context: .
target: nginx
args:
EGEYA_VERSION: ${EGEYA_VERSION:-4199}
EGEYA_URL: ${EGEYA_URL:-}
EGEYA_SHA256: ${EGEYA_SHA256:-}
image: ${EGEYA_NGINX_IMAGE:-local/egeya-nginx}:${EGEYA_VERSION:-4199}
restart: unless-stopped
depends_on:
php:
condition: service_healthy
environment:
NGINX_SERVER_NAME: ${NGINX_SERVER_NAME:-_}
NGINX_CLIENT_MAX_BODY_SIZE: ${PHP_POST_MAX_SIZE:-1024M}
TZ: ${TZ:-UTC}
ports:
- "${HTTP_BIND:-127.0.0.1}:${HTTP_PORT:-8080}:80"
volumes:
- ./data:/var/www/egeya-data:ro
networks:
- frontend
- backend
volumes:
db_data:
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true
secrets:
db_password:
file: ./secrets/db_password
db_root_password:
file: ./secrets/db_root_password
View File
Whitespace-only changes.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+54
View File
@@ -0,0 +1,54 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name ${NGINX_SERVER_NAME};
keepalive_timeout 70;
client_max_body_size ${NGINX_CLIENT_MAX_BODY_SIZE};
root /var/www/html;
index index.php index.html;
location ~* ^.+\.(jpg|jpeg|gif|css|html|htm|png|js|webp|avif|svg|mp3|ogg|mp4|mov)$ {
access_log off;
expires 30d;
}
error_page 404 /errors/404.html;
location / {
log_not_found off;
rewrite ^(.*)$ /index.php?go=$1 last;
}
location ~ ^/(system|user)/.*\.php$ {
deny all;
return 403;
}
location = /user/settings.json {
deny all;
return 403;
}
location = /user/settings.psa {
deny all;
return 403;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param E2_SYNTH_URLS true;
fastcgi_pass php:9000;
fastcgi_index index.php;
}
location ~ /\.ht {
deny all;
}
location ~ \.psa$ {
deny all;
}
}
+71
View File
@@ -0,0 +1,71 @@
#!/bin/bash
set -euo pipefail
EGEYA_DATA_DIR="${EGEYA_DATA_DIR:-/var/www/egeya-data}"
EGEYA_DEFAULTS_DIR="${EGEYA_DEFAULTS_DIR:-/opt/egeya-defaults}"
PHP_RUNTIME_INI="/usr/local/etc/php/conf.d/zz-egeya-runtime.ini"
PHP_MEMORY_LIMIT="${PHP_MEMORY_LIMIT:-512M}"
PHP_UPLOAD_MAX_FILESIZE="${PHP_UPLOAD_MAX_FILESIZE:-512M}"
PHP_POST_MAX_SIZE="${PHP_POST_MAX_SIZE:-1024M}"
PHP_MAX_EXECUTION_TIME="${PHP_MAX_EXECUTION_TIME:-300}"
PHP_MAX_INPUT_TIME="${PHP_MAX_INPUT_TIME:-300}"
copy_defaults_if_empty() {
local name="$1"
local source_dir="${EGEYA_DEFAULTS_DIR}/${name}"
local target_dir="${EGEYA_DATA_DIR}/${name}"
mkdir -p "$target_dir"
if [ -d "$source_dir" ] && [ -z "$(find "$target_dir" -mindepth 1 -print -quit)" ]; then
cp -a "$source_dir"/. "$target_dir"/
fi
}
read_secret_to_env() {
local env_name="$1"
local file_env_name="${env_name}_FILE"
local file_path="${!file_env_name:-}"
if [ -n "$file_path" ]; then
if [ ! -r "$file_path" ]; then
printf 'Secret file is not readable: %s\n' "$file_path" >&2
exit 1
fi
printf -v "$env_name" '%s' "$(cat "$file_path")"
export "$env_name"
fi
}
copy_defaults_if_empty user
copy_defaults_if_empty pictures
mkdir -p \
"$EGEYA_DATA_DIR/tmp" \
"$EGEYA_DATA_DIR/tmp/sessions" \
"$EGEYA_DATA_DIR/user/logs"
touch "$EGEYA_DATA_DIR/user/logs/main.log"
chown -R www-data:www-data \
"$EGEYA_DATA_DIR/user" \
"$EGEYA_DATA_DIR/pictures" \
"$EGEYA_DATA_DIR/tmp"
chmod 0700 "$EGEYA_DATA_DIR/tmp" "$EGEYA_DATA_DIR/tmp/sessions"
read_secret_to_env E2_DB_PASSW
cat > "$PHP_RUNTIME_INI" <<EOF_INI
memory_limit = ${PHP_MEMORY_LIMIT}
upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE}
post_max_size = ${PHP_POST_MAX_SIZE}
max_execution_time = ${PHP_MAX_EXECUTION_TIME}
max_input_time = ${PHP_MAX_INPUT_TIME}
upload_tmp_dir = ${EGEYA_DATA_DIR}/tmp
sys_temp_dir = ${EGEYA_DATA_DIR}/tmp
session.save_path = ${EGEYA_DATA_DIR}/tmp/sessions
EOF_INI
exec /usr/local/bin/docker-php-entrypoint "$@"
+4
View File
@@ -0,0 +1,4 @@
[www]
clear_env = no
catch_workers_output = yes
decorate_workers_output = no
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DUMP_FILE="${1:-${PROJECT_DIR}/backup/egeya.sql}"
if [ ! -r "$DUMP_FILE" ]; then
printf 'SQL dump not found or not readable: %s\n' "$DUMP_FILE" >&2
exit 1
fi
cd "$PROJECT_DIR"
docker compose up -d db
docker compose exec -T db sh -ec '
exec mariadb \
-u"$MARIADB_USER" \
-p"$(cat /run/secrets/db_password)" \
"$MARIADB_DATABASE"
' < "$DUMP_FILE"
printf 'Imported database dump: %s\n' "$DUMP_FILE"
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
set -euo pipefail
SOURCE_BLOG_DIR="${1:-/var/www/blog}"
TARGET_DATA_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/data}"
for name in user pictures; do
source_dir="${SOURCE_BLOG_DIR}/${name}"
target_dir="${TARGET_DATA_DIR}/${name}"
if [ ! -d "$source_dir" ]; then
printf 'Source directory not found: %s\n' "$source_dir" >&2
exit 1
fi
mkdir -p "$target_dir"
rsync -aHAX --delete "$source_dir"/ "$target_dir"/
done
mkdir -p "${TARGET_DATA_DIR}/tmp"
printf 'Copied user and pictures from %s to %s\n' "$SOURCE_BLOG_DIR" "$TARGET_DATA_DIR"
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="${PROJECT_DIR}/.env"
ENV_EXAMPLE="${PROJECT_DIR}/.env.example"
SECRETS_DIR="${PROJECT_DIR}/secrets"
DATA_DIR="${PROJECT_DIR}/data"
create_secret() {
local path="$1"
if [ ! -s "$path" ]; then
umask 077
openssl rand -base64 48 | tr -d '\n' > "$path"
printf '\n' >> "$path"
fi
}
mkdir -p \
"$SECRETS_DIR" \
"$DATA_DIR/user" \
"$DATA_DIR/pictures" \
"$DATA_DIR/tmp"
if [ ! -f "$ENV_FILE" ]; then
cp "$ENV_EXAMPLE" "$ENV_FILE"
fi
create_secret "$SECRETS_DIR/db_password"
create_secret "$SECRETS_DIR/db_root_password"
chmod 0600 \
"$SECRETS_DIR/db_password" \
"$SECRETS_DIR/db_root_password"
printf 'Project prepared: %s\n' "$PROJECT_DIR"
View File
Whitespace-only changes.