I've got a containerized app using docker-compose
to bundle up the backend, the frontend, the database and nginx. Now, I need to add supervisor
but I'm getting some errors while running docker-compose
:
dmc-app | Error: Can't drop privilege as nonroot user
I think I know where the error is coming from but I don't know how to fix it.
Let me first show you my Dockerfile
:
FROM php:8.1.12-fpm
ARG uid=1000
ARG user=inigomontoya
RUN apt-get update && apt-get install -y \
libpng-dev \
libonig-dev \
libxml2-dev \
libzip-dev \
git \
curl \
zip \
unzip \
supervisor
# Install and enable xDebug
RUN pecl install xdebug \
&& docker-php-ext-enable xdebug
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
# Install php modules required by laravel.
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip
# Create system user to run Composer and Artisan commands.
RUN useradd -G www-data,root -u $uid -d /home/$user $user
RUN mkdir -p /home/$user/.composer && \
chown -R $user:$user /home/$user
# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Create directory for supervisor logs
RUN mkdir -p "/etc/supervisor/logs" && chmod -R 775 "/etc/supervisor/logs"
# Set working directory
WORKDIR /var/www
USER $user
# Copy supervisor config files
COPY ./docker/config/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
CMD ["/usr/bin/supervisord"]
and here's my docker-compose
file:
version: "3.9"
services:
app:
build:
context: ./
dockerfile: Dockerfile
image: dmc
container_name: dmc-app
restart: unless-stopped
working_dir: /var/www/
depends_on:
- db
- nginx
volumes:
- ./:/var/www/
- ./docker/php/conf.d/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
- ./docker/php/conf.d/error_reporting.ini:/usr/local/etc/php/conf.d/error_reporting.ini
- ./images:/public/images
expose:
- "9003"
networks:
- dmc-net
nginx:
image: nginx:1.23.2-alpine
container_name: dmc-nginx
restart: unless-stopped
ports:
- "8000:80"
volumes:
- ./:/var/www
- ./docker-compose/nginx:/etc/nginx/conf.d
networks:
- dmc-net
db:
image: mysql:8.0.31
container_name: dmc-db
restart: unless-stopped
# using 3307 on the host machine to avoid collisions in case there's a local MySQL instance installed already.
ports:
- "3307:3306"
# use the variables declared in .env file
environment:
MYSQL_HOST: ${DB_HOST}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: abcd1234
MYSQL_USER: ${DB_USERNAME}
SERVICE_TAGS: dev
SERVICE_NAME: mysql
volumes:
- ./docker-compose/mysql:/docker-entrypoint-initdb.d
- mysql-data:/var/lib/mysql
networks:
- dmc-net
dmc-web:
build:
context: /Users/hansgruber/Desktop/webdev/projects/dundermifflin-ui
dockerfile: /Users/hansgruber/Desktop/webdev/projects/dundermifflin-ui/Dockerfile
target: dev
container_name: dmc-web
restart: always
ports:
- "1313:3000"
- "5137:5137"
- "3000:3000"
volumes:
# it avoids mounting the workspace root
# because it may cause OS specific node_modules folder
# or build folder(.svelte-kit) to be mounted.
# they conflict with the temporary results from docker space.
# this is why many mono repos utilize ./src folder
- /Users/hansgruber/Desktop/webdev/projects/dundermifflin-ui/src:/app/src
- /Users/hansgruber/Desktop/webdev/projects/dundermifflin-ui/static:/app/app/static
- /Users/hansgruber/Desktop/webdev/projects/dundermifflin-ui/static:/app/static/
depends_on:
- app
- nginx
- db
links:
- app
networks:
- dmc-net
networks:
dmc-net:
driver: bridge
volumes:
mysql-data:
here's my supervisord.conf
:
[supervisord]
user=root
logfile=/etc/supervisor/logs/supervisord.log ; main log file; default $CWD/supervisord.log
logfile_maxbytes=5MB ; max main logfile bytes b4 rotation; default 50MB
logfile_backups=10 ; # of main logfile backups; 0 means none, default 10
loglevel=info ; log level; default info; others: debug,warn,trace
pidfile=/var/run/supervisord.pid ; supervisord pidfile; default supervisord.pid
nodaemon=true ; start in foreground if true; default false
minfds=1024 ; min. avail startup file descriptors; default 1024
minprocs=200 ; min. avail process descriptors;default 200
loglevel = INFO
[program:app-worker]
user=root
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=1
redirect_stderr=true
stdout_logfile=/var/www/storage/logs/worker.log
as you can see in my Dockerfile
I've created an user:
ARG user=inigomontoya
...
RUN useradd -G www-data,root -u $uid -d /home/$user $user
and then by the end of the file I'm setting this user:
USER $user
But as far as I understand supervisor
needs to be run by root
.
Here's what I've tried so far:
Attempt 1
I tried replacing user=root
by user=inigomontoya
in supervisord.conf
which is the user I've created in my Dockerfile
but I'm still getting the following error over and over again:
dmc-app | 2024-01-27 05:48:40,690 INFO Included extra file "/etc/supervisor/conf.d/supervisord.conf" during parsing
dmc-app | 2024-01-27 05:48:40,690 INFO Set uid to user 1000 succeeded
dmc-app | Error: Cannot open an HTTP server: socket.error reported errno.EACCES (13)
dmc-app | For help, use /usr/bin/supervisord -h
Attempt 2
Attempted to create a new section in the Dockerfile
where I used an alpine
image and installed supervisor
in it. Then added a new service to my docker-compose
file:
supervisor:
build:
context: ./
dockerfile: Dockerfile
target: supervisor
image: dmc
container_name: dmc-supervisor
networks:
- dmc-net
depends_on:
- app
- nginx
command:
- supervisord
didn't work obviously since it's a separate container and don't have access to php, and as you can see the command executed in supervisor is command=php /var/www/app/artisan queue:work --sleep=3 --tries=3
Attempt 3
Switching user to root
in Dockerfile
right before creating the supervisor logs directory, copying the supervisor files and running CMD:
...
# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
USER root
# Create directory for supervisor logs
RUN mkdir -p "/etc/supervisor/logs" && chmod -R 775 "/etc/supervisor/logs"
# Set working directory
WORKDIR /var/www
# Copy supervisor config files
COPY ./docker/config/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
CMD ["/usr/bin/supervisord"]
This has caused two issues.
supervisor
has started correctly, I don't see any errors in the output related specifically to supervisor
but I do see errors related to one of the programs defined in the conf file [program:app-worker]
:dmc-app | 2024-01-27 06:02:47,589 INFO spawned: 'app-worker_00' with pid 8
dmc-app | 2024-01-27 06:02:47,605 INFO exited: app-worker_00 (exit status 1; not expected)
dmc-app | 2024-01-27 06:02:49,608 INFO spawned: 'app-worker_00' with pid 9
dmc-app | 2024-01-27 06:02:49,621 INFO exited: app-worker_00 (exit status 1; not expected)
dmc-app | 2024-01-27 06:02:52,629 INFO spawned: 'app-worker_00' with pid 10
dmc-app | 2024-01-27 06:02:52,642 INFO exited: app-worker_00 (exit status 1; not expected)
dmc-app | 2024-01-27 06:02:53,644 INFO gave up: app-worker_00 entered FATAL state, too many start retries too quickly
root
instead of the user created initially (inigomontoya
) so my app UI loads but now it cannot connect to the backend:dmc-nginx | 2024/01/27 06:05:35 [error] 21#21: *1 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.65.1, server: dmc-server, request: "GET /api/products?page=1 HTTP/1.1", upstream: "fastcgi://192.168.80.4:9000", host: "host.docker.internal:8000"
dmc-nginx | 192.168.65.1 - - [27/Jan/2024:06:05:35 +0000] "GET /api/products?page=1 HTTP/1.1" 502 157 "-" "undici"
dmc-web | SyntaxError: Unexpected token '<', "<html>
dmc-web | <h"... is not valid JSON
dmc-web | at JSON.parse (<anonymous>)
dmc-web | at parseJSONFromBytes (/app/node_modules/undici/lib/fetch/body.js:579:15)
dmc-web | at successSteps (/app/node_modules/undici/lib/fetch/body.js:519:23)
dmc-web | at fullyReadBody (/app/node_modules/undici/lib/fetch/util.js:863:5)
dmc-web | at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
dmc-web | at async specConsumeBody (/app/node_modules/undici/lib/fetch/body.js:534:3)
dmc-web | at async ProductAPI.fetchData (/app/src/lib/api/api.js:162:16)
dmc-web | at async load (/app/src/routes/products/+page.server.js:8:22)
dmc-web | at async Module.load_server_data (/app/node_modules/@sveltejs/kit/src/runtime/server/page/load_data.js:51:17)
dmc-web | at async Promise.all (index 1)
So the user created in the Dockerfile
is needed for the app to work correctly.
Questions
My main question at this point is: is there a way to use my custom user to run the app and root
to run supervisor
?
So I've read that each docker container should have 1 process, the way I'm trying to solve this is having 2 processes in one container, the one that's basically running the laravel app (backend), is that right? As I mentioned before, I tried to create a separate container to run only supervisor
, but couldn't get it to work since all the files and the executable live on a different container.
Any ideas? Thanks.
You can run multiple containers off the same image. That's usually a better practice than trying to setup supervisord, especially if you have Compose already and especially if you're having trouble getting supervisord to run.
If you're building the image locally, specify the same build:
block in both containers, but override the command:
in one of them (or both). Compose will do the image-build process twice, and docker images
will list two images, but they will be physically the same image (they'll have the same image hash) and the second build will come entirely from cache.
So you can delete all of the supervisord setup in your Dockerfile, and keep your existing non-root user. Your Compose file might look like (simplifying build:
and removing several unnecessary options):
version: "3.8"
services:
app:
build: .
restart: unless-stopped
depends_on:
- db
worker:
build: .
command: /var/www/app/artisan queue:work --sleep=3 --tries=3
restart: unless-stopped
depends_on:
- db
Of the options I've deleted, it's possible you'll need to keep volumes:
in both containers, but in normal use the application code should already get COPY
ed into the image. I've deleted networks:
here, and this needs to be done consistently through the entire file – delete all of the networks:
blocks everywhere.