Deploy A Vue App in Docker Without Root

So you’re going to deploy your Vue app in a Docker container. That great!! Containers are a fantastic way to deploy your app. When I deploy Vue apps, I choose nginx as the web server. nginx is available as a Docker image from Dockerhub, so you don’t need to do much to get started. Unfortunately the default implementation runs in the context of the root user. This can be a security problem, especially if the container gets breached. The attacker is now running as root.

Unfortunately, it’s not quite as simple as just changing the user in the Dockerfile. The reason the nginx image runs as root is that in Linux, the user must be root in order to run the app on port 80 or 443. We can make the changes to the container to make this possible, but the changes are complex. Luckily we are using a container, so the actual port the web server runs on in the container is just not relevant. So we can run the app in the context of a non-root user on any other port (like 8080 for instance). When running the container, we can map back to port 80 or 443 for production deployments if we need to expose the app directly to the Internet. In my case, the SSL/TLS certificate is hosted either in a reverse proxy or a Kubernetes ingress, so I am not including the certificate in my Docker images.

The first thing we need to change is the main configuration file for nginx. We want it to listen on another port, this time it’s going to be 8080. The rest of the configuration is a default setting:, but it could be there if we are exposing the app directly on port 443:

server {
listen 8080;
server_name localhost;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}

error_page 400 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

Next, we need to change the user context nginx runs under. Luckily, the nginx folks have thought about this, and already created a user called nginx right in the default container, so there’s no system-level user configuration necessary. Here’s the complete Dockerfile:

FROM nginx:1.19

RUN rm -f /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d

RUN 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

COPY dist /usr/share/nginx/html

EXPOSE 8080

Let’s look at the relevant parts of the Dockerfile. There are a few directories where the nginx user must have ownership rights for logging, caching and configuration, as well as the process ID file:

RUN 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

We set the user context next, so nginx runs under this user:

USER nginx

Then the Dockerfile copies the contents of the dist folder into the image. This is the output from building our Vue app with npm:

COPY dist /usr/share/nginx/html

And lastly we set the port, which can’t be 80 or 443:

EXPOSE 8080

Now our Dockerfile is set to create a container that is not running with root privileges. The app can be run over 80 or 443 using Docker, a Kubernetes ingress, or even a reverse proxy, with a smaller amount of risk than using the defaults.