Deployment with Docker, Nginx, Gunicorn - No Permissions to Collect Static

Unsure if this should be in Deployment or Mystery Errors, but given it’s an error and I have no idea how to resolve it, I’m putting it in ‘Mystery Errors’.

I’ve deployed my Django web app, and when my entrypoint.sh file attempts to collectstatic, it throws a PermissionError [Errno 13] Permission Denied: '/home/app/web/staticfiles/css/bootstrap.css.map'.

The error:

code-web-1    |   Applying sites.0002_alter_domain_unique...
code-web-1    |  OK  ## Here, all migrations have been applied. On to collect static....
code-web-1    | Traceback (most recent call last):
code-web-1    |   File "/home/app/web/manage.py", line 22, in <module>
code-web-1    |     main()
code-web-1    |   File "/home/app/web/manage.py", line 18, in main
code-web-1    |     execute_from_command_line(sys.argv)
code-web-1    |   File "/usr/local/lib/python3.12/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
code-web-1    |     utility.execute()
code-web-1    |   File "/usr/local/lib/python3.12/site-packages/django/core/management/__init__.py", line 436, in execute
code-web-1    |     self.fetch_command(subcommand).run_from_argv(self.argv)
code-web-1    |   File "/usr/local/lib/python3.12/site-packages/django/core/management/base.py", line 412, in run_from_argv
code-web-1    |     self.execute(*args, **cmd_options)
code-web-1    |   File "/usr/local/lib/python3.12/site-packages/django/core/management/base.py", line 458, in execute
code-web-1    |     output = self.handle(*args, **options)
code-web-1    |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
code-web-1    |   File "/usr/local/lib/python3.12/site-packages/django/contrib/staticfiles/management/commands/collectstatic.py", line 209, in handle
code-web-1    |     collected = self.collect()
code-web-1    |                 ^^^^^^^^^^^^^^
code-web-1    |   File "/usr/local/lib/python3.12/site-packages/django/contrib/staticfiles/management/commands/collectstatic.py", line 135, in collect
code-web-1    |     handler(path, prefixed_path, storage)
code-web-1    |   File "/usr/local/lib/python3.12/site-packages/django/contrib/staticfiles/management/commands/collectstatic.py", line 368, in copy_file
code-web-1    |     if not self.delete_file(path, prefixed_path, source_storage):
code-web-1    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
code-web-1    |   File "/usr/local/lib/python3.12/site-packages/django/contrib/staticfiles/management/commands/collectstatic.py", line 322, in delete_file
code-web-1    |     self.storage.delete(prefixed_path)
code-web-1    |   File "/usr/local/lib/python3.12/site-packages/django/core/files/storage/filesystem.py", line 158, in delete
code-web-1    |     os.remove(name)
code-web-1    | PermissionError: [Errno 13] Permission denied: '/home/app/web/staticfiles/css/bootstrap.css.map'
code-web-1    | [2024-04-08 06:30:09 +0000] [1] [INFO] Starting gunicorn 21.2.0

The Dockerfile:

###########
# BUILDER #
###########

# pull official base image
FROM python:3.12.2-bookworm as builder

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1 \
    PYTHONUNBUFFERED 1

# install system dependencies
RUN apt-get update && \
    apt-get install -y \
    netcat-openbsd \
    graphviz \
    libgraphviz-dev \
    python3-dev \
    gcc \
    libc-dev && \
    pip install pipenv && \
    pip install --upgrade pip

COPY Pipfile Pipfile.lock ./
RUN pipenv install --system --deploy

COPY . .


#########
# FINAL #
#########

# pull official base image
FROM python:3.12.2-bookworm

# create directory for the app user, plus make app user and app group
RUN mkdir -p /home/app && \
    addgroup --system app && \
    adduser --system --group app

# create the appropriate directories
ENV HOME=/home/app \
    APP_HOME=/home/app/web

RUN mkdir -p $APP_HOME/staticfiles $APP_HOME/mediafiles && \
    apt-get update && apt-get install -y \
    netcat-openbsd \
    graphviz \
    libgraphviz-dev \
    python3-dev \
    gcc \
    libc-dev && \
    rm -rf /var/lib/apt/lists/* && \
    chown -R app:app $APP_HOME

RUN chown -R app:app $APP_HOME/staticfiles $APP_HOME/mediafiles

WORKDIR $APP_HOME

# Copy Pipfile and install dependencies
COPY --from=builder /usr/src/app/Pipfile /usr/src/app/Pipfile.lock ./
# Copy installed dependencies from builder
COPY --from=builder /usr/src/app $APP_HOME

RUN pip install pipenv && \
    pipenv install --system --deploy

# copy AND PREPARE entrypoint.prod.sh
COPY ./entrypoint.prod.sh .
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.prod.sh && \
    chmod +x  $APP_HOME/entrypoint.prod.sh

# 
RUN chown -R app:app $APP_HOME/staticfiles $APP_HOME/mediafiles
# change to the app user
USER app

# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]

The docker-compose.yml:

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.prod
    command: gunicorn project.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
    expose:
      - 8000
    env_file:
      - .env-lolz-name-changed
    depends_on:
      - db
  db:
    image: postgres:16
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - .env-lolz-name-changed-lollerskates
  nginx:
      build: ./nginx
      volumes:
        - static_volume:/home/app/web/staticfiles
        - media_volume:/home/app/web/mediafiles
      ports:
        - 1337:80 # For http
        - 443:443 # HTTPS
      depends_on:
        - web
volumes:
  postgres_data:
  static_volume:
  media_volume:

The entrypoint.sh

#!/bin/sh

APPS="app1 app2 app3"

SCRIPTS="script1 script2 script3"


if [ "$DATABASE" = "postgres" ]
then
    echo "Waiting for postgres..."

    while ! nc -z $DB_HOST $DB_PORT; do
      sleep 0.1
    done

    echo "PostgreSQL started"
fi
# Flush the DB
python manage.py flush --no-input
# Make app-specific migrations
for app in $APPS;
do
  echo "Making migrations for $app"
  python manage.py makemigrations "$app"
  sleep 10
done
# Make universal migrations
python manage.py makemigrations
# Migrate apps
for app in $APPS;
do
  echo "Migrating $app"
  python manage.py migrate "$app"
sleep 10
done
# Migrate everything
  python manage.py migrate --no-input
sleep 10
# Collect static ########### THIS IS WHERE THE ERROR OCCURS
python manage.py collectstatic --no-input
# Populate db with scripts
for script in $SCRIPTS;
do
  echo "Executing $script"
  python manage.py "$script"
done

exec "$@"

For some reason, it seems like the Docker build does not execute the chown -R app:app... command, as shown when I enter docker exec -it code-web-1 ls -l /home/app/web/:

-rw-r--r--  1 root root   1170 Mar 29 02:35 Dockerfile
-rw-r--r--  1 root root    889 Apr  8 06:12 Pipfile
-rw-r--r--  1 root root 127830 Apr  8 06:12 Pipfile.lock
-rw-r--r--  1 root root      0 Mar 25 08:18 __init__.py
drwxr-xr-x  3 root root   4096 Mar 26 07:16 audits
drwxr-xr-x  3 root root   4096 Mar 25 08:51 data
-rw-r--r--  1 root root    358 Mar 25 08:24 docker-compose.yml
-rwxr-xr-x  1 root root   1210 Apr  8 07:08 entrypoint.sh
-rwxr-xr-x  1 root root    668 Mar 25 08:24 manage.py
drwxr-xr-x  2 root root   4096 Mar 25 08:51 media
drwxr-xr-x  2 app  app    4096 Apr  8 06:58 mediafiles
-rwxr-xr-x  1 root root   1259 Apr  8 06:12 reset_server.sh
drwxr-xr-x  2 root root   4096 Mar 25 08:23 scripts
drwxr-xr-x  2 root root   4096 Apr  8 06:12 project
drwxr-xr-x  5 root root   4096 Apr  8 06:12 static
drwxr-xr-x 14 root root   4096 Apr  8 07:01 staticfiles
drwxr-xr-x  4 root root   4096 Apr  8 06:12 app1
drwxr-xr-x 12 root root   4096 Apr  8 06:12 app2
drwxr-xr-x  4 root root   4096 Mar 30 03:52 app3

FYI my app names are not just ‘app1 app2 app3’ and the script names are also not ‘script1 script2 script3’; they have meaningful names. But the names here are not the issue; the issue is the permissions.

From the above, mediafiles is the only directory for which user/group app has permissions, which is unexpected. Shouldn’t the recursive chown in the Dockerfile pass permissions down to every file/folder in the target directory?

Any advice on how to remedy this issue? Has anyone else experienced something similar?

I’ve tried the following:

  1. Dumping the containers with the below script, and then rebuilding:
#!/bin/sh

cd code
echo "Stopping the production server"
docker-compose stop
echo "Taking down the production server"
docker-compose down -v
echo "Removing all Docker containers, images, volumes, and networks"
docker system prune -a --volumes
  1. Changing the entrypoint.sh file so that it executes chown on the target directory (it doesn’t have permission because it is being executed by user/group app).

  2. Explicitly adding the target directories to the chown command (earlier it was just chown -R app:app $APP_HOME at the end of the RUN mkdir -p $APP_HOME/staticfiles $APP_HOME/mediafiles && \ in the Final section of the Dockerfile.

Hey, did you resolve the issue? I have the same issue while deploying my Django project

You would need to assign the right permissions to the proper directory structure, along with using the correct account to perform the collect static.

If you’re having a problem similar to this, I suggest you open a new topic with posting the appropriate information about your situation.

(Even though your symptoms may be similar to the original poster, it’s highly unlikely that the details are going to be identical, or that the solution will be the same - the correct solution is going to be highly dependent upon the specifics of your configuration. It’s also likely that working through this will require a number of messages to address everything.)