The syntax nginx expects
Nginx configuration is a tree of directives. Simple directives end with a semicolon (listen 443 ssl;); block directives such as http, server, location, upstream and map wrap their children in braces. Almost every failed reload comes from breaking one of three rules, and the validator checks all three:
- Every simple directive ends with
;. A missing semicolon afterlisten 80makes nginx read the next line as more arguments, which is why its own message often names the wrong directive. The checker reports the directive that is missing the semicolon and the line where it should go. - Braces balance. An unclosed
{is reported against the block that opened it (“opened by ‘http’ on line 1”), and an extra}is reported where it appears. - Quoted strings close. A
add_header Content-Security-Policy "default-src 'self';without its closing double quote swallows the rest of the file.
Regex locations (location ~* \.(js|css)$), variables, # comments and single-quoted strings are all handled. The tokenizer follows nginx’s own rules: a # starts a comment only at the beginning of a token, so location ~ ^/docs/#section keeps its regex intact, and map blocks with an empty '' key parse as nginx reads them.
Checking a snippet without a server
nginx -t is the authoritative test, but it needs nginx installed, the full config tree, and every referenced certificate and include file present. When you are reviewing a pull request, writing an answer for a colleague or editing a single server block in a ticket, that is not available. This checker works on any fragment: a bare location block or a lone server block is fine, no surrounding http required.
Errors are shown with a line and column, the line is highlighted in the editor, and a hint says what to add. While the snippet is broken, the formatted version from your last valid edit stays in the output pane, dimmed. The built-in printer formats the config as soon as it parses, indenting nested blocks consistently. Configs are processed locally; upstream hostnames and internal IPs are not sent anywhere.
What still needs nginx -t
The checker knows nginx grammar, not nginx modules. Directive names are not validated, so a typo like lisen 80; passes here and fails on the server with “unknown directive”. It also does not check argument counts, whether a directive is allowed in its context (proxy_pass inside http), duplicate listen and server_name pairs, include paths, or whether certificate files exist. Templated configs need care as well: Helm or Jinja placeholders such as {{ .Values.host }} should be rendered before checking, because their braces are read as nginx blocks, while envsubst-style ${SERVER_NAME} placeholders look like ordinary nginx variables and pass. Run nginx -t on the target machine before nginx -s reload, and treat this page as the quick first pass that catches the punctuation mistakes.
Examples
Missing semicolon after listen
Invalid: listen 80 has no semicolon, and the error names the listen directive on line 2 rather than the line after it.
server {
listen 80
server_name shop.example.com;
root /var/www/shop;
}Line 2, column 14: Missing ';' after directive 'listen' on line 2Unclosed http block
Invalid: the http block is never closed, so the error points back to line 1 where it was opened.
http {
server {
listen 443 ssl;
location / {
try_files $uri $uri/ /index.html;
}
}
Line 1, column 6: Unclosed '{' opened by 'http' on line 1Unterminated header value
Invalid: the double quote that starts the policy is never closed, so the rest of the block becomes part of the string.
server {
add_header Content-Security-Policy "default-src 'self'; img-src *;
add_header X-Frame-Options DENY;
}Line 2, column 40: Unterminated double-quoted string starting on line 2A valid location fragment
Passes on its own: fragments do not need an enclosing server or http block.
location ~* \.(js|css|png|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}location ~* \.(js|css|png|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
Missing ';' after directive 'listen' on line 2Explained | A simple directive was not terminated with a semicolon. | Add ; at the end of that line. |
Unclosed '{' opened by 'http' on line 1 | A block directive is missing its closing brace, often after copying a nested location. | Add } after the last directive of that block. |
Unexpected '}' with no matching '{' | There is one closing brace too many. | Delete the extra } or restore the block that was removed. |
Unterminated double-quoted string starting on line 2 | A quoted argument has no closing quote. | Close the quote before the semicolon. |
Frequently asked questions
Does this replace nginx -t?
No. nginx -t also checks directive names, contexts, includes and certificate files on the real server. This page catches syntax errors anywhere, without nginx installed.
Why does nginx report a different line than this checker?
When a semicolon is missing, nginx keeps reading arguments and fails on a later token. The checker works out which directive was left open and reports that one.
Can I check OpenResty or Kubernetes ingress snippets?
Yes, as long as they use nginx syntax. In OpenResty configs, the bodies of *_by_lua_block directives are kept verbatim rather than parsed, so Lua code does not trigger semicolon errors.
Are include files followed?
No. An include line is checked as a directive, but the referenced file is not loaded. Paste included files separately.