Rootless container #213

This commit is contained in:
Abdulmhsen B. A. A
2022-07-22 19:02:52 +03:00
parent b04a4f1031
commit 01b0ab8615
20 changed files with 201 additions and 301 deletions

View File

@@ -1,8 +1,6 @@
**/.idea
**/.git
**/vendor
./docker/config/*
!./docker/config/.gitignore
./var/*
!./var/.gitignore
.phpunit.result.cache

View File

@@ -1,31 +1,71 @@
FROM ghcr.io/arabcoders/php_container:latest
FROM alpine:3.16
LABEL maintainer="admin@arabcoders.org"
ENV IN_DOCKER=1
ENV PHP_V=php81
ENV TOOL_PATH=/opt/app
ENV PHP_INI_DIR=/etc/${PHP_V}
RUN mkdir -p /app /config
# Setup the required environment.
#
RUN apk add --no-cache bash caddy nano curl procps net-tools iproute2 shadow sqlite redis tzdata gettext \
${PHP_V} ${PHP_V}-common ${PHP_V}-ctype ${PHP_V}-curl ${PHP_V}-dom ${PHP_V}-fileinfo ${PHP_V}-fpm \
${PHP_V}-intl ${PHP_V}-mbstring ${PHP_V}-opcache ${PHP_V}-pcntl ${PHP_V}-pdo_sqlite ${PHP_V}-phar \
${PHP_V}-posix ${PHP_V}-session ${PHP_V}-shmop ${PHP_V}-simplexml ${PHP_V}-snmp ${PHP_V}-sockets \
${PHP_V}-sodium ${PHP_V}-sysvmsg ${PHP_V}-sysvsem ${PHP_V}-sysvshm ${PHP_V}-tokenizer ${PHP_V}-xml ${PHP_V}-openssl \
${PHP_V}-xmlreader ${PHP_V}-xmlwriter ${PHP_V}-zip ${PHP_V}-pecl-igbinary ${PHP_V}-pecl-redis ${PHP_V}-pecl-xhprof
COPY . /app
# Create user and group
#
RUN deluser redis && deluser caddy && groupmod -g 1588787 users && useradd -u 1000 -U -d /config -s /bin/bash user && \
mkdir -p /config /opt/app && ln -s /usr/bin/php81 /usr/bin/php
RUN usermod -u 1000 www-data && groupmod -g 1000 users && usermod -a -G users www-data && chown -R www-data:users /app && \
runuser -u www-data -- composer --working-dir=/app/ -o --no-progress --no-interaction --no-ansi --no-dev --no-cache --quiet -- install && \
echo '* * * * * /usr/bin/run-app-cron'>>/etc/crontabs/www-data && \
cp /app/docker/files/nginx.conf /etc/nginx/nginx.conf && \
cp /app/docker/files/fpm.conf /usr/local/etc/php-fpm.d/docker.conf && \
cp /app/docker/files/entrypoint.sh /usr/bin/entrypoint-docker && \
cp /app/docker/files/app_console.sh /usr/bin/console && \
cp /app/docker/files/cron.sh /usr/bin/run-app-cron && \
cp /app/docker/files/redis.conf /etc/redis.conf && \
rm -rf /app/docker/ /app/var/ /app/.github/ && \
chmod +x /usr/bin/run-app-cron /usr/bin/console /usr/bin/entrypoint-docker && \
chown -R www-data:users /app /config /var/lib/nginx/ && \
sed -i 's/group = www-data/group = users/' /usr/local/etc/php-fpm.d/www.conf
# Copy tool files.
#
COPY ./ /opt/app
ENTRYPOINT ["/usr/bin/entrypoint-docker"]
# install composer & packages.
#
ADD https://getcomposer.org/download/latest-stable/composer.phar /opt/composer
RUN chmod +x /opt/composer && \
/opt/composer --working-dir=/opt/app/ -o --no-progress --no-interaction --no-ansi --no-dev --no-cache --quiet -- install && \
rm /opt/composer
# Copy configuration files to the expected directories.
#
RUN ln -s ${TOOL_PATH}/bin/console /usr/bin/console && \
cp ${TOOL_PATH}/container/files/cron.sh /opt/job-runner && \
cp ${TOOL_PATH}/container/files/Caddyfile /opt/Caddyfile && \
cp ${TOOL_PATH}/container/files/redis.conf /opt/redis.conf && \
cp ${TOOL_PATH}/container/files/init-container.sh /opt/init-container && \
cp ${TOOL_PATH}/container/files/fpm.conf /etc/${PHP_V}/php-fpm.d/z-container.conf && \
rm -rf ${TOOL_PATH}/{container,var,.github,.git} && \
sed -i 's/user = nobody/; user = user/' /etc/${PHP_V}/php-fpm.d/www.conf && \
sed -i 's/group = nobody/; group = users/' /etc/${PHP_V}/php-fpm.d/www.conf
# Change Permissions.
#
RUN chmod +x /usr/bin/console /opt/init-container /opt/job-runner && \
chown -R user:user /config /opt /etc/${PHP_V} /var/run
# Set the entrypoint.
#
ENTRYPOINT ["/opt/init-container"]
# Change working directory.
#
WORKDIR /config
EXPOSE 9000 8081 80
# Switch to user
#
USER user
CMD ["php-fpm"]
# Expose the ports.
#
EXPOSE 9000 8081
# Run php-fpm
#
CMD ["php-fpm81"]

9
FAQ.md
View File

@@ -53,8 +53,8 @@ $ php console
The app should save your data into `./var` directory. If you want to change the directory you can export the environment
variable `WS_DATA_PATH` for console and browser. you can add a file called `.env` in main tool directory with the
environment variables. take look at the files inside `docker/files` directory to know how to run the scheduled tasks and
if you want a webhook support you would need a frontend proxy for `php8.1-fpm` like nginx, caddy or apache.
environment variables. take look at the files inside `container/files` directory to know how to run the scheduled tasks
and if you want a webhook support you would need a frontend proxy for `php8.1-fpm` like nginx, caddy or apache.
---
@@ -174,12 +174,9 @@ via the `docker-compose.yaml` file.
| Key | Type | Description | Default |
|------------------|---------|----------------------------------------------|---------|
| WS_DISABLE_CHOWN | integer | Do not change ownership `/config` directory. | `0` |
| WS_DISABLE_HTTP | integer | Disable included `HTTP Server`. | `0` |
| WS_DISABLE_CRON | integer | Disable included `Task Scheduler`. | `0` |
| WS_DISABLE_CACHE | integer | Disable included `Cache Server`. | `0` |
| WS_UID | integer | Set container user id. | `1000` |
| WS_GID | integer | Set container group id. | `1000` |
---
@@ -243,7 +240,7 @@ Go to your Plex Web UI > Settings > Your Account > Webhooks > (Click ADD WEBHOOK
Click `Save Changes`
**Note:** If you use multiple plex servers and use the same PlexPass account for all of them, you have to unify the API
**Note**: If you use multiple plex servers and use the same PlexPass account for all of them, you have to unify the API
key, by
running the following command:

View File

@@ -23,12 +23,14 @@ version: '3.3'
services:
watchstate:
image: ghcr.io/arabcoders/watchstate:latest
# If you want to change the default uid/gid mapping enable the user option.
#user: "${UID:-1000}:${GID:-1000}"
container_name: watchstate
restart: unless-stopped
# For information about supported environment variables visit FAQ page.
# works for both global and container specific environment variables.
environment:
- WS_UID=${UID:-1000} # Set container user id.
- WS_TZ=Asia/Kuwait${UID:-1000} # Set container user id.
- WS_GID=${GID:-1000} # Set container group id.
ports:
- "8081:8081" # webhook listener port.
@@ -36,7 +38,20 @@ services:
- ./data:/config:rw # mount current directory to container /config directory.
```
**Note:** Using port `80` is deprecated and will be removed in v1 release.
----
## Breaking change since 2022-07-22
We fully switched to rootless container, as such we have to rebuild the container.
Things that need to be adjusted:
* Default webhook listener port is now `8081` instead of `80`. If you were using the webhook functionality you have to
change the port.
* If you changed the default gid/uid `1000` You have to use the `user:` directive instead of `WS_GID`, `WS_UID`. The
example above has the syntax for it just uncomment `#user:` and add your uid:gid
-----
After creating your docker compose file, start the container.
@@ -87,9 +102,8 @@ please refer to [Environment variables list](FAQ.md#environment-variables).
* On demand.
* Webhooks.
### Note:
Even if all your backends support webhooks, you should keep import task enabled. This help keep healthy relationship.
**Note**: Even if all your backends support webhooks, you should keep import task enabled. This help keep healthy
relationship.
and pick up any missed events.
---

57
bin/console Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
if (function_exists('posix_geteuid') && 0 === posix_geteuid()) {
fwrite(STDERR, 'Running this tool as user 0 "root" is not allowed. Please choose different user.' . PHP_EOL);
exit(1);
}
error_reporting(E_ALL);
ini_set('display_errors', 'On');
require __DIR__ . '/../pre_init.php';
set_error_handler(function (int $number, mixed $error, mixed $file, int $line) {
$errno = $number & error_reporting();
static $errorLevels = [
E_ERROR => 'Error',
E_WARNING => 'Warning',
E_PARSE => 'Parser Error',
E_NOTICE => 'Notice',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User notice',
E_STRICT => 'Strict Notice',
E_RECOVERABLE_ERROR => 'Recoverable Error'
];
if (0 === $errno) {
return;
}
$message = sprintf('%s: %s (%s:%d).' . PHP_EOL, $errorLevels[$number] ?? (string)$number, $error, $file, $line);
fwrite(STDERR, trim($message) . PHP_EOL);
exit(1);
});
set_exception_handler(function (Throwable $e) {
$message = sprintf("%s: %s (%s:%d)." . PHP_EOL, get_class($e), $e->getMessage(), $e->getFile(), $e->getLine());
fwrite(STDERR, trim($message) . PHP_EOL);
exit(1);
});
if (!file_exists(__DIR__ . '/../vendor/autoload.php')) {
fwrite(STDERR, 'Dependencies are missing.' . PHP_EOL);
exit(1);
}
require __DIR__ . '/../vendor/autoload.php';
(new App\Libs\Initializer())->boot()->runConsole();

67
console
View File

@@ -2,65 +2,8 @@
declare(strict_types=1);
if (function_exists('posix_geteuid') && 0 === posix_geteuid()) {
fwrite(STDERR, 'Unable to run as root. Please choose different user.' . PHP_EOL);
exit(1);
}
error_reporting(E_ALL);
ini_set('display_errors', 'On');
require __DIR__ . '/pre_init.php';
set_error_handler(function (int $number, mixed $error, mixed $file, int $line) {
$errno = $number & error_reporting();
static $errorLevels = [
E_ERROR => 'Error',
E_WARNING => 'Warning',
E_PARSE => 'Parser Error',
E_NOTICE => 'Notice',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User notice',
E_STRICT => 'Strict Notice',
E_RECOVERABLE_ERROR => 'Recoverable Error'
];
if (0 === $errno) {
return;
}
fwrite(
STDERR,
trim(
sprintf('%s: %s (%s:%d)' . PHP_EOL, ($errorLevels[$number] ?? (string)$number), $error, $file, $line)
) . PHP_EOL
);
exit(1);
});
set_exception_handler(function (Throwable $e) {
fwrite(
STDERR,
trim(
sprintf("%s: %s (%s:%d)." . PHP_EOL, get_class($e), $e->getMessage(), $e->getFile(), $e->getLine())
) . PHP_EOL
);
exit(1);
});
if (!file_exists(__DIR__ . '/vendor/autoload.php')) {
fwrite(STDERR, 'Composer dependencies are missing. Run the following commands.' . PHP_EOL);
fwrite(STDERR, sprintf('cd %s', __DIR__) . PHP_EOL);
fwrite(STDERR, 'composer install --optimize-autoloader' . PHP_EOL);
exit(1);
}
require __DIR__ . '/vendor/autoload.php';
(new App\Libs\Initializer())->boot()->runConsole();
/**
* @RELEASE
* This file is deprecated and will be removed in v1.
*/
require __DIR__ . '/bin/console';

View File

@@ -0,0 +1,6 @@
http://:8081 {
root * /opt/app/public
php_fastcgi 127.0.0.1:9000
file_server
log
}

15
container/files/cron.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Exit if already running.
#
if [[ "`pidof -x $(basename $0) -o %PPID`" ]]; then
exit;
fi
# Loop until the sun explode.
#
while true
do
sleep 60
/usr/bin/console system:tasks --run --save-log
done

16
container/files/fpm.conf Normal file
View File

@@ -0,0 +1,16 @@
[global]
daemonize=no
error_log=/proc/self/fd/2
log_limit=8192
[www]
pm=dynamic
pm.max_children=10
pm.start_servers=1
pm.min_spare_servers=1
pm.max_spare_servers=3
pm.max_requests=1000
listen=9000
clear_env=no
catch_workers_output=yes
decorate_workers_output=no

View File

@@ -17,66 +17,28 @@ else
echo "[${TIME_DATE}] INFO: No environment file present at [${ENV_FILE}]."
fi
WS_UID=${WS_UID:-1000}
WS_GID=${WS_GID:-1000}
WS_DISABLE_CHOWN=${WS_DISABLE_CHOWN:-0}
WS_DISABLE_HTTP=${WS_DISABLE_HTTP:-0}
WS_DISABLE_CRON=${WS_DISABLE_CRON:-0}
WS_DISABLE_CACHE=${WS_DISABLE_CACHE:-0}
set -u
if [ "${WS_UID}" != "$(id -u www-data)" ]; then
usermod -u "${WS_UID}" www-data
if [ 0 = "${WS_DISABLE_CACHE}" ]; then
TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z")
echo "[${TIME_DATE}] Starting Cache Server."
redis-server "/opt/redis.conf"
fi
if [ "${WS_GID}" != "$(getent group users | cut -d: -f3)" ]; then
groupmod -g "${WS_GID}" users
fi
if [ ! -f "/app/vendor/autoload.php" ]; then
if [ ! -f "/usr/bin/composer" ]; then
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer
fi
runuser -u www-data -- composer install --working-dir=/app/ --optimize-autoloader --no-ansi --no-progress --no-dev
fi
if [ ! -f "/usr/bin/console" ]; then
cp /app/docker/files/app_console.sh /usr/bin/console
chmod +x /usr/bin/console
fi
if [ ! -f "/usr/bin/run-app-cron" ]; then
cp /app/docker/files/cron.sh /usr/bin/run-app-cron
chmod +x /usr/bin/run-app-cron
fi
if [ 0 = "${WS_DISABLE_CHOWN}" ]; then
if ! grep '/app' /proc/mounts; then
chown -R www-data:users /app
fi
chown -R www-data:users /config /var/lib/nginx/ /etc/redis.conf
fi
# Generate config structure.
/usr/bin/console --version >>/dev/null
if [ 0 = "${WS_DISABLE_HTTP}" ]; then
TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z")
echo "[${TIME_DATE}] Starting HTTP Server."
nginx
caddy start --config /opt/Caddyfile
fi
if [ 0 = "${WS_DISABLE_CRON}" ]; then
TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z")
echo "[${TIME_DATE}] Starting Task Scheduler."
/usr/sbin/crond -b -l 2
fi
if [ 0 = "${WS_DISABLE_CACHE}" ]; then
TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z")
echo "[${TIME_DATE}] Starting Cache Server."
runuser -u www-data -- redis-server "/etc/redis.conf"
/opt/job-runner &
fi
TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z")
@@ -99,7 +61,7 @@ TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z")
echo "[${TIME_DATE}] Running - $(/usr/bin/console --version)"
/usr/bin/console system:php >"${PHP_INI_DIR}/conf.d/zz-app-custom-ini-settings.ini"
/usr/bin/console system:php --fpm >"${PHP_INI_DIR}/../php-fpm.d/zzz-app-pool-settings.conf"
/usr/bin/console system:php --fpm >"${PHP_INI_DIR}/php-fpm.d/zzz-app-pool-settings.conf"
# first arg is `-f` or `--some-option`
if [ "${1#-}" != "$1" ]; then

View File

@@ -0,0 +1,8 @@
daemonize yes
pidfile /opt/redis.pid
bind 127.0.0.1
port 6379
# Logging
logfile ""

View File

@@ -1 +0,0 @@
*

View File

@@ -1,17 +0,0 @@
version: '3.3'
services:
watchstate:
container_name: watchstate
restart: unless-stopped
build: ../
environment:
WS_UID: ${UID:-1000}
WS_GID: ${GID:-1000}
WS_CRON_IMPORT: 1
WS_CRON_EXPORT: 1
WS_CRON_EXPORT_AT: '*/2 * * * *'
ports:
- "8081:80"
volumes:
- ../:/app
- ./config:/config:rw

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env sh
UID=$(id -u)
if [ "0" == "${UID}" ]; then
runuser -u www-data -- php /app/console "${@}"
else
php /app/console "${@}"
fi

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env sh
UID=$(id -u)
if [ 0 == "${UID}" ]; then
runuser -u www-data -- /usr/bin/console system:tasks --run --save-log
else
/usr/bin/console system:tasks --run --save-log
fi

View File

@@ -1,8 +0,0 @@
[global]
error_log = /proc/self/fd/2
log_limit = 8192
[www]
clear_env = no
catch_workers_output = yes
decorate_workers_output = no

View File

@@ -1,85 +0,0 @@
user www-data;
worker_processes auto;
pcre_jit on;
error_log /dev/stderr warn;
include /etc/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
client_max_body_size 10m;
sendfile on;
tcp_nopush on;
gzip_vary on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
log_format traceable '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" "$request_id"';
server {
listen 8081 default_server;
listen [::]:8081 default_server;
# @RELEASE - remove port
listen 80 default_server;
listen [::]:80 default_server;
error_log /dev/stderr warn;
access_log /dev/stdout traceable;
add_header X-Request-Id $request_id always;
root /app/public;
index index.html index.php;
charset utf-8;
location = /favicon.ico {
access_log off;
log_not_found off;
return 204;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~* (index|test)\.php$ {
# regex to split $uri to $fastcgi_script_name and $fastcgi_path
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
# Check that the PHP script exists before passing it
try_files $fastcgi_script_name =404;
# Bypass the fact that try_files resets $fastcgi_path_info
# see: http://trac.nginx.org/nginx/ticket/321
set $path_info $fastcgi_path_info;
fastcgi_param PATH_INFO $path_info;
# For better request tracking.
fastcgi_param X_REQUEST_ID $request_id;
fastcgi_index index.php;
include fastcgi.conf;
fastcgi_pass 127.0.0.1:9000;
}
location ~ /\.ht {
deny all;
}
}
}

View File

@@ -1,32 +0,0 @@
daemonize yes
pidfile /var/run/redis/redis.pid
bind 127.0.0.1
port 6379
# Logging
logfile ""
# Persistance
dbfilename redis.rdb
dir /config/cache/
appendonly no
appendfilename appendonly.aof
save 900 1
save 300 10
save 60 10000
# Arbitrary Parameters
maxmemory-policy allkeys-lru
slowlog-log-slower-than 10000
slowlog-max-len 128
notify-keyspace-events ""
# Plan Properties:
timeout 3600
tcp-keepalive 60
# DO NOT EDIT THIS LINE KEEP IT AS IT IS.
# USE ENV VARIABLE TO CHANGE PASSWORD OR REQUIRE PASS.
#requirepass replace_me

View File

@@ -37,4 +37,9 @@ set_exception_handler(function (Throwable $e) {
exit(Command::FAILURE);
});
// -- In case the frontend proxy does not generate request unique id.
if (!isset($_SERVER['X_REQUEST_ID'])) {
$_SERVER['X_REQUEST_ID'] = bin2hex(random_bytes(16));
}
(new App\Libs\Initializer())->boot()->runHttp();

View File

@@ -681,7 +681,7 @@ final class Initializer
[
'request' => [
'id' => ag($params, 'X_REQUEST_ID'),
'ip' => ag($params, 'REMOTE_ADDR'),
'ip' => ag($params, ['X_FORWARDED_FOR', 'REMOTE_ADDR']),
'agent' => ag($params, 'HTTP_USER_AGENT'),
'uri' => (string)$uri,
],