Payload on a subdomain or subpath cover image

How to host PayloadCMS on a subdomain and subpath with nginx

Sometimes your tech stack's complexity is a little more advanced and you need to host an API on a subdomain or a subpath (or both!) and it can get confusing if, like me, you only do devops when you have to.

The configurations we're going to cover here aren't in any way specific to Payload or even nginx, so you will be able to apply these same principles to other apps and servers such as apache2 or caddy.

We will cover both subdomain and subpath hosting to cover the most common use cases and we're going to use our prototyping space nouance.dev as the example primary domain.

Prerequisites

  • a configured server with Payload running on it, see the end for resources on this
  • base knowledge of nginx and server configuration

Test your deployment works by checking that you can reach your Payload site via your server's IP and the port, usually something like <ip>:3000/admin.

DNS configuration

Let's get the most uncertain part out of the way and configure our DNS. Depending on your provider, these changes might propagate pretty quickly but in some cases it can also take up to 24 hours. If you're doing a lot of DNS changes and rapidly prototyping setups, we recommend a service like Cloudflare which is often very fast to propagate your changes worldwide.

We will create a new A record with the host being api and then pointing it to our server's IP address.

Subdomain hosting

This is by far the easiest to set up and not at all that different to domain hosting and you may already have this step working, so you can skip further down where we move Payload to be hosted on a subpath.

Payload configuration

Set your serverURL in the payload.config.ts file as we did here:

1// payload.config.ts
2
3
4// ...
5
6export default buildConfig({
7 serverURL: 'https://api.nouance.dev',
8
9// ...
typescript

Nginx configuration

Below is roughly the nginx configuration you're looking to run. After this is setup you can run certbot and it will make some changes to it on its own to add a redirect from http to https and for it to listen on port 443 instead of 80.

1server {
2 listen 80;
3
4 server_name api.nouance.dev;
5
6 location / {
7 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
8 proxy_set_header Host $host;
9 proxy_pass http://127.0.0.1:3000;
10 proxy_http_version 1.1;
11 # proxy_buffering off; # we will come back to this
12 proxy_set_header Upgrade $http_upgrade;
13 proxy_set_header Connection "upgrade";
14 }
15}
nginx

We've commented out proxy buffering for now to leave it "on" by default. We will explain below in our troubleshooting step how it works and what you can do if you encounter issues.

Rebuild Payload to make sure your updated configuration is being used and then restart nginx with service nginx restart (it may need sudo).

That's it! Make sure your server is running and now you should be able to access it via your subdomain.

Subpath hosting

One common requirement is that you may want all of your APIs or endpoints to exist on the same subdomain but to then access them on a different subpath.

Here is a hypothetical tech structure:

  • api.nouance.dev/ <- our documentation site

    • api.nouance.dev/payload <- Payload
    • api.nouance.dev/chat <- chat bot API

These subpaths can be anything and you can choose what you need. We recommend separate subdomains as it tends to simplify things a lot for your developers.

Payload configuration

To support subpaths we will need to change our payload configuration with the following custom routes being set, see the docs for a full explanation.

1// payload.config.ts
2
3// ...
4
5routes: {
6 api: '/payload/api',
7 admin: '/payload/admin',
8 graphQL: '/payload/graphql',
9 graphQLPlayground: '/payload/graphql-playground',
10},
11
12// ...
typescript

Nginx configuration

Now we should go back to our nginx configuration to catch only /payload locations for this particular reverse proxy. Simply change your / location to /payload and then rebuild your Payload app to have the updated config and restart nginx and that's it!

1// payload.conf
2
3// ...
4
5location /payload {
6
7// ...
nginx

What if my app doesn't run on a subpath?

So there's a third way to host content on a subpath, this doesn't apply to Payload due to the above configuration of the routes, however if we want to serve a subpath to a localhost that doesn't take it we can actually configure our subpath to rewrite the internal URL.

1// payload.conf
2
3// ...
4
5location /myapp {
6 rewrite ^/myapp/(.*)$ /$1 break;
7 // ... same configuration as before ...
8}
9
10// ...
nginx

So this little rewrite line will now take any path starting with /myapp and rewrite internally to / so that our localhost application responds to it as expected and you can use this to serve static files too.

DigitalOcean has a great tutorial expanding on this nginx directive.

Useful tips and troubleshooting

Remember that certain TLDs such as .dev require https connections by default in modern browsers, so while it can be tempting to leave the cert generation until the end we recommend you get it out of the way sooner than that.

Adding more locations/paths

In the same server block you can add multiple locations if you want to reverse proxy to other applications on your server. Just remember that nginx' location matching works in order from top to bottom and uses regex matching, so if you want to have a / catch all it has to be at the end.

Proxy buffering

We said we'd come back to this. This part here seems to highly depend on many factors but if you're having issues properly accessing Payload's API or admin panel, you might find it useful to uncomment that line and disable proxy buffering via proxy_buffering off;.

In nginx, proxy buffering is a form of cache nginx uses to cache responses from your proxied endpoint, so it can serve them directly from its own memory cache which improves performance a lot as your localhost app may not need to be hit again however this can also cause issues when dealing with a more dynamic API such as Payload's depending on what you're doing.

As you can imagine it wouldn't be an issue if you're just fetching static data, but otherwise it can cause you to receive expired data or, more commonly noticed, incorrectly cached proxy responses as we've seen in a few situations.

X-Accel headers

In direct continuation with the above, you can also bypass nginx' response buffering cache via a header added to your requests.

Set X-Accel-Buffering to "no", so for example res.setHeader("X-Accel-Buffering", "no");.

This tells nginx to turn off response caching for this request. You can view the full list of available headers from nginx here.

Resources

If you found this useful, follow us on Twitter to keep up to date with our future work and let us know what other areas of the tech stack we could help out with.