dockernginxdocker-composedockerfilesupervisord

Cannot run supervisor in Docker as a non-root user


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.

  1. I believe 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
  1. I believe the app is running now as 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

Any ideas? Thanks.


Solution

  • 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 COPYed 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.