Can Strapi run in a reverse proxied container using NGinx?

URLs start with HTP to avoid “new users can only put in 2 links”

I really hope the answer to my question is yes since we have committed to a switch to Strapi and this is a make or break requirement.

I have been working on this for the better part of a day and I am no stranger to Docker, Nginx, etc. I am convinced Strapi can work this way but wow, what a bizarre problem. It should be super simple and I am wondering now if Strapi is doing something unusual in the backend?

Goal:
docker-compose.yml runs several self-contained Strapi instances, one per client, each in their own container. The same docker-compose also includes nginx and portainer. The root of Nginx is bound to HTP://cms.ourdomain.com (HTTPS and certs will be added after this issue is solved). Each strapi instance is reverse_proxied as a subdirectory. HTTP://cms.ourdomain.com/client-1, HTP://cms.ourdomain.com/client-2, etc.

The best that I can achieve is the “Let’s get started” screen BUT, only the HTML has been returned and rendered. All images are broken and a quick inspection shows that all other URL have NOT been proxied. For example, instead of seeing cms.ourdomain.com/client-1/favicon.ico I see cms.ourdomain.com/favicon.ico. Put another way, it seems that only the first request is proxied.

FWIW this is being developed in Ubuntu 20.04 in WSL2 but will be deployed to AWS infrastructure.

docker-compose.yml

version: "3"

services:
  
  proxy:
    container_name: proxy.cms.ourdomain.com
    image: nginx:stable
    volumes:
      - ./nginx/conf.d/strapi.conf:/etc/nginx/conf.d/strapi.conf

    ports:
      - 80:80
      - 443:443

  dashboard:
    container_name: dashboard.cms.ourdomain.com
    image: portainer/portainer-ce
    ports:
      - 9000:9000
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

  client-1:
    container_name: client-1.cms.ourdomain.com 
    image: strapi/strapi
    ports:
      - 1337:1337

strapi.conf

server {
  listen 80;
  server_name cms.ourdomain.com;

  location /client-1/ {
    proxy_pass http://172.24.163.100:1337/;
  }
}
  
1 Like

Hi @tforster

Yes Strapi can containerized and sitting behind Nginx (it’s recommended especially for SSL and Load Balancing). However there are few key things to note.

First see the following Nginx sub-domain example config here: https://strapi.io/documentation/v3.x/deployment/nginx-proxy.html#nginx-virtual-host

Likewise you will need to let Strapi know that it’s setting behind a proxy: https://strapi.io/documentation/v3.x/deployment/nginx-proxy.html#strapi-server

2 Likes

@DMehaffy Thank you for responding. I saw those links before but discarded them as they seemed unusually complex for setting up a simple Docker container and not relevant to my simple use-case. I will return to them later this morning. Meanwhile, a couple of (hopefully) quick questions:

  1. The documentation clearly describes the how but not the why. What is the rationale for creating the upstream.conf and could I not simply proxy_pass 127.0.0.1:1337; in the location block of strapi.conf?
  2. What is it about the Strapi architecture that requires a code change, as illustrated in config/server.js? This is not a critique, just a genuine question to understand the architecture.
  3. Is the code snippet shown for config/server.js the entire code for that file? Presumably, that would be added in COPY clause of a custom Strapi Dockerfile? Or is the intent that one docker-compose ups the stack, which won’t work right away, and then edits config/server.js in the Strapi container?

Cheers

This is a general best practice, usually you would have a single Nginx instance to handle multiple domains, ect and it’s much easier to maintain a single upstream config as Nginx will complain if you have multiple upstream blocks spread across multiple virtual host configs. The upstream block is generally used for load balancing and scaling, by hard coding the ip:port in the virtual host file you will be unable to scale the number of instances.

See this for some examples of upstream usage: Module ngx_http_upstream_module

There are two main keys that can modify core parts of the Strapi variables to properly load routes:

server.js => url => This is used to tell the Strapi React admin panel how to connect to the backend and is compiled into the React admin panel during the build phase (the backend is not built). It’s also used throughout the application as a variable for things like the email plugin (injecting the public link into password reset emails) and the 3rd party auth providers (for redirects)

server.js => admin.url => this is optional and is used to change the route to access the admin panel, say from /admin to /dashboard and is relative (usually) combined with the above url

It is not, only as an example, you should read through the following documentation to understand the structure of the server.js config file

1 Like

@DMehaffy Thanks for those answers, they helped immensely.

I am able to run my environment using the Sub-Folder Split example which is closest to my use-case where I need to expose a dashboard to content managers and consume an API from the existing site build process.

However, my hope is to be able to deploy the dashboard such that the URL is “rooted” a level above the API. For example https://mydomain.com/client-01 and https://mydomain.com/client-02 would both resolve to the client-specific dashboards with https://mydomain.com/client-01/api and https://mydomain.com/client-02/api resolving to the respective APIs. Doing so provides a better experience for the non-technical content managers and editors as they will intuitively go to the respective client folder.

Unfortunately. I have not been able to get this to work despite editing the paths in server.js (and making corresponding changes to the nginx conf files). I can achieve

https://mydomain.com/client-01/dashboard
https://mydomain.com/client-01/api

But not

https://mydomain.com/client-01/
https://mydomain.com/client-01/api

To simplify things I have tried without the client-01 folder with

# Strapi API
location /api/ {
    rewrite ^/api/(.*)$ /$1 break;
    proxy_pass http://strapi;
    ...snip...
  }

  # Strapi Dashboard
  location / {
    proxy_pass http://strapi/dashboard;
    ...snip...
  }

and

module.exports = ({ env }) => ({
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1337),
  url: 'http://mydomain.com/api',
  admin: {
    url: 'http://mydomain.com/',
  },
});

you need to adjust this line, it’s specifically replacing the /api/ with just / so if you are adding onto the path you need to adjust it to something like

rewrite ^/client-01/api/(.*)$ /$1 break;

All the rewrite is doing is removing that string from the request sent to Strapi.

If possible can you just provide your full virtual host config file (removing any private information like SSH key path and actual domain)?

In my most recent example I was testing right off the root of my domain. E.g. I removed all the client sub folder references to reduce the possibility of other issues. So I believe rewrite ^/api/(.*)$ /$1 break; would be correct for https://cms.mydomain.com/api?

Silly question but I don’t know what you mean by virtual host file. Are you referring to the nginx conf file? Here’s everything regardless. The NGinx image is from nginx:stable although I did try nginx:latest with no difference in results. Both upstream.conf and strapi.conf are being loaded from conf.d, not sites-enabled.

FWIW I am rebuilding Strapi from inside the container with strapi build after I make changes as well as reloading nginx with nginx -s reload. I am seeing lots of Uncaught SyntaxError: Unexpected token '<' in the browser console.

nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/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;
}

upstream.conf

# Strapi server
upstream strapi {
    server 172.24.163.100:1337;
}

strapi.conf

server {
  # Listen HTTPS
  listen 80;
  server_name cms.mydomain.com;

  # Strapi API
  location /api/ {
    rewrite ^/api/(.*)$ /$1 break;
    proxy_pass http://strapi;
    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;
  }

  # Strapi Dashboard
  location / {
    proxy_pass http://strapi/dashboard;
    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;
  }
}

server.js

module.exports = ({ env }) => ({
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1337),
  url: 'http://cms.mydomain.com/api',
  admin: {
    url: 'http://cms.mydomain.com/',
    auth: {
      secret: env('ADMIN_JWT_SECRET', '***'),
    },
  },
});

Let me take a look, your strapi.conf is what is referred to as a virtual host config as it’s bound to a specific server_name. Generally you would have multiple files based on a server_name instead of just throwing all of these into the default nginx.conf. And no biggie on where it’s loaded from, the sites-enabled is a debian thing, I know most other distros (or whatever your docker container is based on) don’t have that folder so conf.d is fine :wink:

Another common alias for “virtual host” is “server block” with both referring to running multiple “virtual hosts” on the same port, IE port 80 or port 443, ect.

Give me a few minutes to test out your config

I assume you are trying to serve the admin panel on the root, and I’m not sure the best way to handle this, I’ll have to do some testing.

Yes, but to clarify, for simple testing. I was eliminating the client subfolders in an attempt to get things working, and then I was going to add the client subfolders back in. But the premise remains, the dashboard is served from the “client” root and the API from the client /api.

To simplify testing

http://cms.mydomain.com/ (dashboard)
http://cms.mydomain.com/api

Final goal

http://cms.mydomain.com/client-01 (dashboard)
http://cms.mydomain.com/client-01/api
http://cms.mydomain.com/client-02 (dashboard)
http://cms.mydomain.com/client-02/api

etc…

I’ll try and setup an example based on your end goal and see if I can get that to you tomorrow

1 Like

Hi, @DMehaffy I know you are busy but wondering if you had a chance to look into this particular scenario? Alternatively, if it is something not currently supported I can live with that and work around it.

I haven’t lately but I’ll try and get to it today, give me a little bit. Lots of meetings on Fridays :laughing:

I’m sorry this got thrown in my backlog and I accidently skipped over it, I’ll setup a test instance right now to do some testing.

Short term I would suggest using the /admin or /dashboard and simply putting a 301 redirect html file in the Strapi public folder as index.html to redirect to the actual admin path.

FYI I am working on an example but I did hit a bug that needs to be fixed before I can give a full example.

Pending that I have a working example using the /client-01/admin and /client-02/admin

Root domain: http://strapi.guru/ (left a little message)

Client 1 Root: http://strapi.guru/client-01
Client 1 Admin: http://strapi.guru/client-01/admin/auth/login
Client 1 Blog Posts: http://strapi.guru/client-01/api/blog-posts
Client 1 Server.js File: http://strapi.guru/client1-server.js

Client 2 Root: http://strapi.guru/client-02/
Client 2 Admin: http://strapi.guru/client-02/admin
Client 2 Blog Posts: http://strapi.guru/client-02/api/blog-posts
Client 2 Server.js File: http://strapi.guru/client2-server.js

(changed to .txt just so browser wouldn’t try to download them)
Nginx Upstream: http://strapi.guru/upstream.conf.txt
Nginx Conf: http://strapi.guru/strapi.conf.txt

Note in that config I am currently trying to static serve the admin instead of allowing Strapi to serve it to bypass the restrictions for serving the admin on the root, this is also a good idea if you plan to scale the Strapi backend, cuts load from the backend for serving the admin and you only need to build the admin once and serve it from Nginx

WIP:

# Ignore me pending fix for https://github.com/strapi/strapi/issues/8616
#    location /client-01/ {
#        rewrite ^/client-01/admin/(.*)$ /client-01/$1 break;
#        root /var/www/html;
#    }

Once the bug is fixed I will try again on the static admin served from the /client-01 and /client-02 root.

Also dropping the files here too in case someone comes around to this thread and I have already deleted that test server:

Configs

/etc/nginx/conf.d/upstream.conf

upstream client1 {
    server 127.0.0.1:1338;
}

upstream client2 {
    server 127.0.0.1:1339;
}

/etc/nginx/sites-enabled/strapi.conf

server {

# Listen HTTP
    listen 80;
    server_name strapi.guru;

# Static Root
    location / {
        root /var/www/html;
    }

# Client 1
   location /client-01/api/ {
        rewrite ^/client-01/api/(.*)$ /$1 break;
        proxy_pass http://client1;
        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;
    }

    location /client-01/ {
        rewrite ^/client-01/(.*)$ /$1 break;
        proxy_pass http://client1;
        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;
    }

# Ignore me pending fix for https://github.com/strapi/strapi/issues/8616
#    location /client-01/ {
#        rewrite ^/client-01/admin/(.*)$ /client-01/$1 break;
#        root /var/www/html;
#    }

# Client 2
    location /client-02/api/ {
        rewrite ^/client-02/api/(.*)$ /$1 break;
        proxy_pass http://client2;
        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;
    }

    location /client-02/ {
        rewrite ^/client-02/(.*)$ /$1 break;
        proxy_pass http://client2;
        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;
    }
}

/srv/strapi/strapi-client1/config/server.js

module.exports = ({ env }) => ({
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1338),
  url: env('NGINX_URL', 'http://strapi.guru/client-01'),
  admin: {
    auth: {
      secret: env('ADMIN_JWT_SECRET'),
    },
  },
});

/srv/strapi/strapi-client2/config/server.js

module.exports = ({ env }) => ({
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1339),
  url: env('NGINX_URL', 'http://strapi.guru/client-02'),
  admin: {
    auth: {
      secret: env('ADMIN_JWT_SECRET'),
    },
  },
});

And just so you know I’m not joshing you:

Also feel free to sign in to those two admin panels and create some content types, I’ll leave the server up for a few days while I wait for our engineering team to take a look at the bug report and so I can apply some changes and make a copy of the config for future examples :wink:

email: test@test.com
password: Test1234!
1 Like

@DMehaffy, in light of the bug that you have uncovered, my team can certainly live with the requirement of navigating to the admin folder for now. It is not a make-or-break issue, just a nice-to-have convenience.

I can update the NGinx conf file in the future when the bug has been resolved. Meanwhile, I will keep an eye on this space for any updates.

Cheers.

@DMehaffy

with
nginx.conf

upstream strapi  { server strapi.example.com:12345; }
server {
	server_name example.com;
	...
	location /cms/ {
		rewrite ^/cms/(.*)$ /$1 break;
		proxy_pass http://strapi;
		...
	}
	location /cms/api/ {
		rewrite ^/cms/api/(.*)$ /$1 break;
		proxy_pass http://strapi;
		...
	}
	...

and
server.js

 module.exports = ({ env }) => ({
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1337),
  url: env('NGINX_URL', 'https://example.com/cms'),
  admin: {
    auth: {
      secret: env(
        'ADMIN_JWT_SECRET',
        'x...'
      ),
    },
  },
});

I can access

API Root: h t t p s://example.com/cms
Admin: h t t p s://example.com/cms/admin

I followed the issue links, and am unclear on where the thread currently ends ;-/

Is this config yet cleanly doable?

API Root: h t t p s://example.com/cms/api
Admin: h t t p s://example.com/cms

No, not currently, you can’t mount the admin on the root (or what Strapi sees as the root).

You would have to use:

You could replace the normal index.html in the public folder with a simple html refresh redirect to the admin path though instead of showing the status page.