Nginx Configuration

Depfloy generates and manages a nginx configuration for every project. The Nginx tab on a project lets you layer your own customisations on top of Depfloy's defaults without editing nginx files over SSH.

When to use it #

You’ll need the Nginx tab when you want to:

  • Add custom HTTP headers
  • Increase upload size or proxy buffer sizes
  • Add a custom error page or redirect
  • Add a location block with special handling
  • Proxy a subpath to a separate service

For most projects, the defaults are fine — you don’t need to touch this tab at all.

What the generated configuration already contains #

Worth knowing before you add anything, because roughly half of what people paste is already there — and the other half is missing for a reason you may want to override.

Node.js projectsStatic sites
X-Frame-Options, X-XSS-Protection, X-Content-Type-OptionsSetSet
Static asset caching (expires 1y, Cache-Control: public, immutable)Not setSet
gzipNot setNot set
Dotfile deny (.git, .env, everything except .well-known)SetSet

A Node.js project’s assets are served by the application rather than by nginx, which is why the cache header belongs to the framework’s own output and not to this file. gzip is not set anywhere; if you want it, the Server directives box is where it goes.

When the configuration is regenerated #

The file on the server is not rewritten on every deploy. Depfloy renders it when something that shapes it changes:

  • the project is created
  • a domain is added or removed
  • a certificate is created, renewed or deleted
  • you press Save snippets, Save & override or Restore template
  • you turn markdown negotiation on or off
  • a Node.js project’s build switches between a server build and a static one

A normal deployment is not on that list, which matters when Depfloy improves the template itself: an existing project keeps the file it already has until one of the events above happens. The Publish card at the bottom of the Nginx tab is there for exactly this — it re-renders from your current saved state, validates with nginx -t and reloads, without you having to change anything first.

The generated file opens with a line naming the project:

# DEPFLOY-PROJECT: 232

Leave it in place if you write a full custom config. It is how Depfloy recognises which file belongs to which project.

The three boxes #

The Nginx tab, under Managed snippets, gives you three text areas. Which one you pick decides where your directives land, and putting a location block in the wrong one is the mistake that costs the most time — nested inside another location, it matches nothing and nginx still starts cleanly.

  • Server directives — injected inside the server block. Directives that apply to the whole virtual host: client_max_body_size, add_header, gzip, error_page.
  • Location directives — injected inside the project’s main location / block. Directives that should apply only to the application’s own route: proxy timeouts, buffer sizes.
  • Extra locations — whole location blocks appended to the server block. This is where redirects, health checks and anything matching a path other than / belong.

Press Save snippets and Depfloy regenerates the configuration on the server and reloads nginx. The change is live immediately; there is no deployment to run afterwards.

# Server directives — bigger uploads, custom security header
client_max_body_size 50M;
add_header X-Custom-Header "Custom Value";

# Location directives — extra proxy header for the app's own route
proxy_set_header X-Real-IP $remote_addr;
# Extra locations — a redirect and a health check
location = /old-page { return 301 /new-page$is_args$args; }

location /healthz {
    return 200 'ok';
}

return does not carry the query string the way rewrite does. A redirect written as return 301 /new-page; drops ?ref=newsletter and everything after it, so append $is_args$args whenever the parameters matter to you.

Proxying to another hostname #

A proxy_pass pointing at a fixed hostname is the one snippet that can take down more than the project it belongs to. nginx resolves such a hostname once, at start-up, and a name it cannot resolve makes it reject the whole configuration — every site on that server, not just this one, fails to come up.

Depfloy saves the snippet anyway and shows an amber warning next to it, along with the pattern that resolves per request instead:

location /api {
    resolver 1.1.1.1 valid=30s ipv6=off;
    resolver_timeout 5s;

    set $upstream_host api.example.com;
    proxy_pass https://$upstream_host$request_uri;
    proxy_ssl_name $upstream_host;
    proxy_ssl_server_name on;
    proxy_set_header Host $upstream_host;
}

A variable target needs a resolver in scope or every request to that location returns 502. Depfloy adds one to the server block when it sees a variable proxy_pass, so the warning is about the fixed-hostname form, not this one.

Custom snippets can override Depfloy’s defaults #

Most nginx directives can only appear once in a server or location block — client_max_body_size, keepalive_timeout, gzip, and similar. If both Depfloy’s template and your snippet set the same directive, nginx fails with a “directive is duplicate” error.

To make custom snippets work cleanly, Depfloy detects when your snippet sets one of these single-instance directives and removes Depfloy’s default for that directive before injecting your custom block. Your snippet wins; nginx stays valid.

Directives that can be overridden #

If your snippet sets one of these directives, Depfloy drops its own copy in favour of yours:

client_max_body_size, client_body_buffer_size, client_body_timeout, client_header_buffer_size, client_header_timeout, large_client_header_buffers, keepalive_timeout, send_timeout, proxy_read_timeout, proxy_send_timeout, proxy_connect_timeout, proxy_buffer_size, proxy_buffers, proxy_busy_buffers_size, proxy_buffering, fastcgi_read_timeout, fastcgi_send_timeout, fastcgi_buffer_size, fastcgi_buffers, server_tokens, charset, root, index, autoindex, gzip, gzip_types, gzip_min_length, gzip_proxied, gzip_comp_level.

Directives that are always kept #

Directives that can legitimately appear multiple times (add_header, set_real_ip_from, listen, server_name, log_format, location, if, and others) are not affected. Your snippet’s directives are appended alongside Depfloy’s.

Where it applies #

The override behaviour only applies to the Server directives and Location directives boxes. Anything in Extra locations is appended as written — a duplicate location there is rejected by nginx rather than reconciled, and the save fails with the error.

Advanced: full custom config #

The three boxes add to Depfloy’s template. When you need to change something the template itself sets, the Advanced: full custom config section at the bottom of the tab replaces it: you paste entire nginx server block(s) and Depfloy writes those to the server instead of generating anything.

Two things change the moment it is active, and the second one surprises people:

  1. Depfloy stops generating the configuration for this project.
  2. The managed snippets are ignored. Not merged into your config, not appended — ignored. Anything you had in Server directives, Location directives or Extra locations stops applying, and the page tells you so rather than failing.

Use Load current config first. It fills the editor with what is serving your site right now, so you edit a working configuration instead of writing one from memory. Starting from an empty editor means reproducing every default yourself — the SSL certificate paths, the ACME challenge location, the deny rule for dotfiles, and on a Node.js project the entire proxy block. Missing one of those does not fail validation; it fails traffic.

Saving is the same shape as saving snippets: Save & override writes the file, validates it, and reloads nginx. A config nginx rejects is not loaded, so a syntax error cannot take the site down.

The three boxes keep what you typed while a custom config is active. They are locked and their contents are not applied — your pasted config replaces the template they would have been rendered into — but they are still there when you go back.

Going back #

Restore template in the Publish card discards the full custom config and serves Depfloy’s managed template again, with your snippets rendered back into it. It is only available while a custom config is active.

Straight afterwards, Undo restore puts the custom config back — useful when the template turns out not to cover something you had in there. The undo stays available until you save a new custom config or new snippets; either of those is a fresh decision, and it goes away.

Load current config fills the editor with the file serving your site right now. That file already contains your managed snippets, because snippets are rendered into the template rather than kept in a separate file — so starting from it carries them into the custom config as plain directives.

When not to reach for it #

A redirect, a header, a health check, a location block for some other path — all of these belong in the three boxes above, and they keep working when Depfloy improves its template. Full custom config is for the cases where the template’s own output is in your way.

On a Node.js project #

A custom config survives deployments, but the application port does not. Use the upstream alias described under Node.js Upstream rather than a port number, or the config that worked when you saved it starts returning 502 after the next deploy.

Applying a change, and the restart you rarely need #

The project menu has two nginx actions, and they are not interchangeable.

Apply nginx config re-reads the configuration while nginx keeps serving. A configuration nginx does not accept is rejected and the running one stays in place, so this cannot take the site down. It is what saving snippets and pressing Publish already do, and what an AI assistant gets when it calls restart_service for nginx.

Force restart nginx stops nginx and starts it again. On the way up it re-reads every site on that server, so a problem in any one of them — a hostname that no longer resolves, a deleted certificate — keeps all of them down. The one case that needs it is picking up a patched system library, which a reload cannot do because it does not replace the master process.

Maintenance Mode interaction #

When a project is in Maintenance Mode, the nginx tab is read-only — the configuration is locked to the maintenance page until you turn maintenance off. You can still view what would be deployed, you just can’t save changes until the project is out of maintenance.

Plugin error visibility #

If you have a Laravel plugin installed on the project (Octane, Reverb, Horizon, Nightwatch — see Background Jobs) and its setup or runtime hits an error, the plugin row shows an Error badge with a destructive banner below it. The banner contains:

  • A truncated single-line error message with Show full error to expand
  • A Copy button to grab the error text
  • A Logs button that opens the full plugin log

This is especially useful when an nginx-related setup fails because a directive collides with something — the override behaviour above usually catches that, but if not, the banner tells you exactly which directive nginx rejected.

Node.js Upstream #

Node.js projects (Next.js, Nuxt, Remix, React Router) use blue/green deployment, which means the application port changes between deployments. Depfloy automatically manages an nginx upstream block for each Node.js project:

upstream upstream_{project_id} {
    server 127.0.0.1:{port} max_fails=2 fail_timeout=5s;
    keepalive 32;
}

If you write a custom nginx configuration that references a Node.js project, always use the upstream alias instead of a hardcoded port number. The port will change on every deployment and a hardcoded port will cause 502 errors.

# ✅ Correct — uses upstream alias, port updates automatically
location /docs {
    proxy_pass http://upstream_232;
    proxy_http_version 1.1;
    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;
}

# ❌ Wrong — hardcoded port will break after deployment
location /docs {
    proxy_pass http://127.0.0.1:3093;
}

You can find your project’s upstream name in the nginx configuration file. It follows the pattern upstream_{project_id}. The project ID is visible in the URL when you open the project in Depfloy.