Nginx proxy configuration for Strapi admin panel fails to serve assets at /cms/admin

System Information
  • Strapi Version: 5.1.0
  • Operating System: ubuntu
  • Database: postgres

Hi,

I’m trying to set up a Strapi backend alongside a React frontend using Nginx as a reverse proxy. The frontend is accessible at https://pel.example.org, and I want the Strapi admin panel to be accessible at https://pel.example.org/cms/admin. Everything seemed to be moving right until this point. The admin panel fails to load static assets, showing errors like (below) and also a blank white page.

https://pel.example.org/admin/strapi-DBHkT-2C.js net::ERR_ABORTED 404 (Not Found)

The assets seem to be requested at /admin/... instead of /cms/admin/....

My Setup

  • Frontend: React app running in a Docker container (frontend), accessible at https://pel.example.org.
  • Backend: Strapi app running in a Docker container (backend), intended to be accessible at https://pel.example.org/cms. The docker logs show that strapi starts successfully.
  • Nginx: Acts as the reverse proxy for both services.

Here’s my Nginx configuration and config/server.js file:

server {
    listen 80;
    listen [::]:80;

    server_name pel.example.org www.pel.example.org;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://pel.example.org$request_uri;
    }
}

# HTTPS server
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name pel.example.org;

    ssl_certificate /etc/nginx/ssl/live/pel.example.org/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/live/pel.example.org/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    # Frontend location
    location / {
        proxy_pass http://frontend:80;
        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-Proto $scheme;
    }

    location /cms/admin {
        rewrite ^/cms/admin/(.*)$ /admin/$1 break;
        proxy_pass http://backend:1337/admin/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
    
    location /cms/ {
        rewrite ^/cms/?(.*)$ /$1 break;
        proxy_pass http://backend:1337/;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $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-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass_request_headers on;
        
        client_max_body_size 100M;
        proxy_read_timeout 300;
        proxy_connect_timeout 300;
        proxy_send_timeout 300;
    }
}

Strapi server.js Configuration

module.exports = ({ env }) => ({
  host: env("HOST", "0.0.0.0"),
  port: env.int("PORT", 1337),
  url: 'https://pel.example.org/cms',
  app: {
    keys: env.array("APP_KEYS"),
  },
  webhooks: {
    populateRelations: env.bool("WEBHOOKS_POPULATE_RELATIONS", false),
  },
  transfer: {
    remote: {
      enabled: true,
    },
  },
});

What I’ve Tried

  1. I’ve ensured the admin panel is built by running npm run build inside the container.
  2. The frontend successfully accesses the backend’s assets at /cms.
  3. Checked for similar topics in the forums but none has helped.

Despite this, requests to https://pel.example.org/cms/admin fail due to missing static assets.

Questions

  1. How can I correctly configure Nginx to serve the Strapi admin panel at /cms/admin?
  2. Is there any additional configuration needed in Strapi’s server.js or elsewhere to handle /cms as a prefix?
  3. What could cause the assets to request paths like /admin/... instead of /cms/admin/...?

@DMehaffy @tgalle Any guidance would be appreciated. Thank you! :blush:

I was experiencing a similar issue with my deployment to Digital Ocean App Platform (which uses Nginx) and found I had set ‘Preserve Path Prefix’ to true. Disabling this feature restored expected functionality. Here’s a screenshot of the setting in DO:

Hopefully this will help point you in the right direction.

The following solved my 404 errors and allowed Strapi to correctly resolve its entry file via a subdirectory installation.

TLDR:

If your Strapi installation is in a subdirectory of your main domain, define your admin url as an absolute path.

e.g. url: 'https://example-domain.com/cms/admin'


config/admin.(js|ts)

export default ({ env }) => ({

  # DASHBOARD_URL must be an absolute path if your
  # Strapi install is in a subdirectory, regardless of whether
  # or not you are using a custom /admin url

  url: env('DASHBOARD_URL'), # https://example-domain.com/cms/admin

  auth: {
    secret: env('ADMIN_JWT_SECRET'),
  },
  apiToken: {
    salt: env('API_TOKEN_SALT'),
  },
  transfer: {
    token: {
      salt: env('TRANSFER_TOKEN_SALT'),
    },
  },
})

For Docker users:

Make sure you pass --build-arg PUBLIC_URL="https://example-domain.com/cms" (including the subdirectory, /cms) to your docker build command, and add the ARG and ENV to your Dockerfile at the top of the corresponding build stage.

Build command

(DigitalOcean will handle this for you, granted you have defined your env variables in App Platform)

docker build \
  --tag "my-strapi-image" \
  --build-arg PUBLIC_URL="https://example-domain.com/cms" \
  --build-arg ADMIN_URL="https://example-domain.com/cms/admin" \
  .

Dockerfile (partial)

# Install stage
...

# Build stage
FROM base as builder
# Make sure ARGs are declared via --build-arg flags in your 'docker build' command
ARG NODE_ENV
ARG DATABASE_URL
ARG PUBLIC_URL # https://example-domain.com/cms
ARG ADMIN_URL # https://example-domain.com/cms/admin

ENV GENERATE_SOURCEMAP=false
ENV NODE_OPTIONS=--max_old_space_size=4096
ENV DATABASE_URL=$DATABASE_URL
ENV ADMIN_URL=$ADMIN_URL
ENV PUBLIC_URL=$PUBLIC_URL
ENV NODE_ENV=$NODE_ENV

WORKDIR /app
COPY --from=installer /app .

RUN npm config set fetch-retry-maxtimeout 600000 --global

RUN npm run build

# Runner stage
...

config/server.(js|ts)

export default ({ env }) => ({
  proxy: true,
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1337),
  url: env('PUBLIC_URL', `http://localhost:${env.int('PORT', 1337)}`),
  app: {
    keys: env.array('APP_KEYS'),
  },
  emitErrors: true,
})