How to run nginx as non-privileged user with Docker
nginx is an open-source solution for web serving and reverse proxying your web application. You put it “in front” of your different services, and nginx can route the traffic to the correct url.
That’s useful for micro-services, for example.
Per default, nginx runs as root user. Why?
Only root processes can listen to ports below 1024. The default port for web applications is usually 80 or 443.
But that shouldn’t be a detriment to running Docker as a non-privileged user. After all, we can forward ports.
That’s the -p 80:8080
syntax that you might have seen in a docker run
command.
You map the TCP port 8080 from the Docker container to port 80 on the Docker host (for example, your nginx webserver that listens to port 80).
For security reasons, it’s better to run a Docker container as a non-root user.
So, how can we achieve that?
Pull The Default Docker Image for nginx
Let’s say we have a React application and a backend written with Python and Flask.
During development, we might have used the official Docker node image and the official Docker python image.
Now, for deployment with nginx, we’ll use the official nginx Docker image.
I prefer the slimmed-down Debian distribution instead of the Alpine images that most people seem to use.
Most of the time, Alpine doesn’t contain essential tools and packages, and you’ll have to painstakingly debug why your installation doesn’t work.
The slim version of Debian (currently “Buster Slim”) ships with better defaults, and is only slightly larger.
For a better understanding, I recommend the article The best Docker base image for your Python application.
Your Dockerfile
will start like this:
FROM nginx:1.17.6
The good news is that the official Docker build for nginx already installs a non-root user called nginx
.
The bad news is that the nginx
user doesn’t have all the permissions it needs to run your program.
Adjust nginx Configuration
You’ll need to replace the standard nginx configuration: the /etc/nginx/nginx.conf
file and the /etc/nginx/conf.d/default.conf
.
For the nginx.conf
file, you should check how the default config looks like, and delete the user
directive (first line of the file).
Here’s the /etc/ngnix/nginx.conf
that works with Debian:
## user user; ## <- delete this line
worker_processes 1;
error_log /var/nginx/log/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
Create your nginx configuration file, etc/nginx/conf.d/default.conf
.
Example:
server {
listen $PORT;
root /usr/share/nginx/html;
index index.html index.html;
location / {
try_files $uri /index.html =404;
}
location /auth {
proxy_pass https://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_redirect default;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
location /users {
proxy_pass https://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_redirect default;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
You can see the files on GitHub if you like.
The Python flask application runs on port 5000 inside the Docker container and has two routes: /auth
and /users
.
Only the front-end client will query these routes. Your users won’t access them.
nginx will serve the React application from the root route (/
) to the public.
Now, copy those two customized configurations into your Docker container.
For the Flask application, we’ll also need to install Python.
FROM nginx:1.17.6
## install python3
RUN apt-get update && \
apt-get install -y --no-install-recommends python3
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./default.conf /etc/nginx/conf.d/default.conf
Add User Permissions
We need to give the nginx
user permissions to several files.
Our working directory on the docker container will be /app
. We’ll copy the source code from our local machine into that folder later.
The nginx
user needs permission for the WORKDIR
and also for /var/cache/nginx
(cache), /etc/nginx/conf.d
(for the nginx configuration), and the tmp
folder (for pid
and logging).
We have to create some of those files within the Dockerfile
, otherwise, the container won’t run.
FROM nginx:1.17.6
RUN apt-get update && \
apt-get install -y --no-install-recommends python3
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./default.conf /etc/nginx/conf.d/default.conf
WORKDIR /app
## add permissions for nginx user
RUN chown -R nginx:nginx /app && chmod -R 755 /app && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
chown -R nginx:nginx /etc/nginx/conf.d
RUN touch /var/run/nginx.pid && \
chown -R nginx:nginx /var/run/nginx.pid
USER nginx
That’s the bulk of our work.
Now we can copy our local code into the container, set environment variables, and run the start command.
Example:
FROM nginx:1.17.6
RUN apt-get update && \
apt-get install -y --no-install-recommends python3
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./default.conf /etc/nginx/conf.d/default.conf
WORKDIR /app
## add permissions
RUN chown -R nginx:nginx /app && chmod -R 755 /app && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
chown -R nginx:nginx /etc/nginx/conf.d
RUN touch /var/run/nginx.pid && \
chown -R nginx:nginx /var/run/nginx.pid
## switch to non-root user
USER nginx
## add Python app
COPY . .
## set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV FLASK_ENV production
ENV SECRET_KEY $SECRET_KEY
## run server
CMD gunicorn -b 0.0.0.0:5000 manage:app --daemon && \
sed -i -e 's/$PORT/'"$PORT"'/g' /etc/nginx/conf.d/default.conf && \
nginx -g 'daemon off;'
When you run the container, don’t forget to use ports higher than 1024.
Example:
docker run -d -p 8007:5000 my-nginx-app
If you want to take a look at a working multi-stage docker build, you can check my deploy Dockerfile (for Heroku) for the Flask React Auth course by Testdriven.io.
Recap
Deploying nginx with Docker as non-root-user is possible, and improves the security of your Docker containers.
You have to jump through some hoops to set the correct permissions for the user, but then it works like a charm.
Further Reading
- Nginx in Docker without Root by PJ Dietz
- Running Nginx as non root user on StackOverflow
- Nginx in Docker as non root user by Jozef Bilka
- Why non-root containers are important for security on Bitnami
- The best Docker base image for your Python application by Itamar Turner-Tauring
- Official Docker image for nginx
- Authentication with Flask, React, and Docker by Michael Herman