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 **/.idea
**/.git **/.git
**/vendor **/vendor
./docker/config/*
!./docker/config/.gitignore
./var/* ./var/*
!./var/.gitignore !./var/.gitignore
.phpunit.result.cache .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" LABEL maintainer="admin@arabcoders.org"
ENV IN_DOCKER=1 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 && \ # Copy tool files.
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 && \ COPY ./ /opt/app
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
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 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 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 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 environment variables. take look at the files inside `container/files` directory to know how to run the scheduled tasks
if you want a webhook support you would need a frontend proxy for `php8.1-fpm` like nginx, caddy or apache. 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 | | 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_HTTP | integer | Disable included `HTTP Server`. | `0` |
| WS_DISABLE_CRON | integer | Disable included `Task Scheduler`. | `0` | | WS_DISABLE_CRON | integer | Disable included `Task Scheduler`. | `0` |
| WS_DISABLE_CACHE | integer | Disable included `Cache Server`. | `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` 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 key, by
running the following command: running the following command:

View File

@@ -23,12 +23,14 @@ version: '3.3'
services: services:
watchstate: watchstate:
image: ghcr.io/arabcoders/watchstate:latest 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 container_name: watchstate
restart: unless-stopped restart: unless-stopped
# For information about supported environment variables visit FAQ page. # For information about supported environment variables visit FAQ page.
# works for both global and container specific environment variables. # works for both global and container specific environment variables.
environment: 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. - WS_GID=${GID:-1000} # Set container group id.
ports: ports:
- "8081:8081" # webhook listener port. - "8081:8081" # webhook listener port.
@@ -36,7 +38,20 @@ services:
- ./data:/config:rw # mount current directory to container /config directory. - ./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. 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. * On demand.
* Webhooks. * Webhooks.
### Note: **Note**: Even if all your backends support webhooks, you should keep import task enabled. This help keep healthy
relationship.
Even if all your backends support webhooks, you should keep import task enabled. This help keep healthy relationship.
and pick up any missed events. 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); 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); * @RELEASE
exit(1); * This file is deprecated and will be removed in v1.
} */
require __DIR__ . '/bin/console';
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();

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}]." echo "[${TIME_DATE}] INFO: No environment file present at [${ENV_FILE}]."
fi 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_HTTP=${WS_DISABLE_HTTP:-0}
WS_DISABLE_CRON=${WS_DISABLE_CRON:-0} WS_DISABLE_CRON=${WS_DISABLE_CRON:-0}
WS_DISABLE_CACHE=${WS_DISABLE_CACHE:-0} WS_DISABLE_CACHE=${WS_DISABLE_CACHE:-0}
set -u set -u
if [ "${WS_UID}" != "$(id -u www-data)" ]; then if [ 0 = "${WS_DISABLE_CACHE}" ]; then
usermod -u "${WS_UID}" www-data TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z")
echo "[${TIME_DATE}] Starting Cache Server."
redis-server "/opt/redis.conf"
fi 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 if [ 0 = "${WS_DISABLE_HTTP}" ]; then
TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z") TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z")
echo "[${TIME_DATE}] Starting HTTP Server." echo "[${TIME_DATE}] Starting HTTP Server."
nginx caddy start --config /opt/Caddyfile
fi fi
if [ 0 = "${WS_DISABLE_CRON}" ]; then if [ 0 = "${WS_DISABLE_CRON}" ]; then
TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z") TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z")
echo "[${TIME_DATE}] Starting Task Scheduler." echo "[${TIME_DATE}] Starting Task Scheduler."
/usr/sbin/crond -b -l 2 /opt/job-runner &
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"
fi fi
TIME_DATE=$(date +"%Y-%m-%dT%H:%M:%S%z") 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)" 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 >"${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` # first arg is `-f` or `--some-option`
if [ "${1#-}" != "$1" ]; then 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); 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(); (new App\Libs\Initializer())->boot()->runHttp();

View File

@@ -681,7 +681,7 @@ final class Initializer
[ [
'request' => [ 'request' => [
'id' => ag($params, 'X_REQUEST_ID'), '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'), 'agent' => ag($params, 'HTTP_USER_AGENT'),
'uri' => (string)$uri, 'uri' => (string)$uri,
], ],