Ingress and TLS 13 November 2026 · edition 17 min read

Publishing a new site behind Nginx in three stages: HTTP, certificate, HTTPS

Putting a new site behind a shared Nginx is three changes with three different preconditions. Hostwatch applies them as separate stages, tests every Nginx write and restores the previous file on failure, and leaves DNS and the moment of cutover to the operator.

Published 13 November 2026. Hostwatch engineering review; implementation limits are stated below.

Three stone plinths in a row joined by a light conduit: a server miniature with a bare port, a glowing glass capsule, and a server miniature with a glass sleeve over its port
EDITORIAL IMAGEOriginal AI-generated conceptual illustration, not a product screenshot or production measurement. The HTTP proxy, the certificate and the HTTPS block are separate steps, and each one rests on the step before it. Open full size ↗
01 / 10

One task hides three changes with different preconditions

A new application is running in a container and listening on a loopback port, which is a port bound to 127.0.0.1 and reachable only from the server itself. To serve it at its own domain through the Nginx that already fronts the other sites on that server, three things have to change. Nginx needs a server block that routes the domain to the port. A certificate authority, in this article Let's Encrypt, has to issue a certificate for the domain. Nginx then needs a TLS server block that uses that certificate. Most people think of this as one task, putting the site online, and many setups do it with one script.

The three changes depend on different things. The first depends only on the server: the domain must not belong to another site there, and there must be a port to proxy to. The second depends on the public internet, because the authority has to reach the server under that domain, and that requires DNS to point at the server already. The third depends on the second, since the certificate files have to exist before Nginx is told to load them. DNS is edited by the operator at a DNS provider and spreads through resolver caches at its own pace. Validation happens at the authority. Neither finishes on the server's schedule, so a single script has to either wait in the middle without knowing for how long, or fail halfway and leave the configuration between two steps.

Hostwatch performs the three changes as separate stages of one operation called component_ingress, which a workspace owner calls through the Hostwatch MCP server. Ingress here means the public route into an application, and the stages are named http, certificate and https. Each stage checks its own preconditions, is previewed before it is applied, and leaves the server in a state that is still valid if nothing else happens for a day. The sections below follow the stages in order. They describe what the Hostwatch agent, the program installed on the server, checks at each one, and what remains the operator's decision. The reasoning is the same for anyone who does this by hand with Nginx and Certbot.

02 / 10

What goes wrong when the order is wrong

Suppose the HTTPS block is written first. Nginx reads certificate and key files when it loads its configuration, and it loads all sites as one configuration. If one server block names a certificate file that does not exist, the configuration test fails for everything. What happens next depends on how the change is applied. A reload of a running Nginx is rejected and the process keeps serving with the configuration it already had, as the Nginx documentation on controlling the server describes, so nothing looks broken. The invalid file stays on disk, though. From then on every reload fails, including the one a colleague runs next week for an unrelated site, and the next restart or reboot leaves Nginx unable to start. On a shared server that takes down every site because of one that was never online.

Suppose instead the certificate is requested first. The usual way to prove control of a domain to Let's Encrypt is the HTTP-01 challenge of ACME, the protocol used for automatic issuance. The ACME client, here Certbot, places a small file under a well-known path, and the authority fetches it over plain HTTP on port 80, finding the server through the domain's public DNS. If no server block answers that path yet, or DNS still leads to another machine, the fetch fails and no certificate is issued. Let's Encrypt also limits how many failed validations it accepts for a name in a given period, so retrying in a loop while DNS settles uses up an allowance that is needed later.

That leaves one workable order: the HTTP server block, then the DNS change, then the certificate, then the HTTPS block. Each step ends in a configuration that passes the Nginx test. After the first, the site answers over plain HTTP and the challenge path exists. After the certificate is issued, Nginx has not changed at all; there is only a new set of files in the certificate store. After the last, both ports are configured. If the work stops between two steps because DNS has not moved yet or somebody went home, the server is still consistent and the other sites are unaffected.

Five server miniatures on one long stone plinth share a single light conduit that is dark because a glass connector at one end has an empty socket
EDITORIAL IMAGEOriginal AI-generated conceptual illustration, not a product screenshot or production measurement. One server block that references a missing certificate fails the configuration shared by every site on the server. Open full size ↗
03 / 10

Stage http: a proxy and the challenge path, before DNS moves

The http stage writes one file into the Nginx conf.d directory, named after the component. A component in this article is a managed component: a container target that Hostwatch created from an operator's template, with a fixed loopback port. The file starts with a comment that marks it as managed by Hostwatch and names the stage. It contains a single server block that listens on port 80 over IPv4 and IPv6 for the requested domains. The ACME challenge path is served as static files from the server's shared webroot, the directory where Certbot will later place its challenge file. Everything else is proxied to the loopback port with the original host, client address and scheme passed in headers, a 60-second read timeout and a 16 MB limit on request bodies. Access logging for the site is off unless the server has Hostwatch's private access log format installed. The caller supplies the domains and nothing else: no path, no port and no line of Nginx configuration.

Before writing, the agent checks the request. It accepts one to four domains, each an ordinary host name, so wildcard names and bare addresses are rejected. It scans the files in the sites-enabled and conf.d directories for server_name lines. If a requested domain already appears in another file, the preview lists that as a blocker, which is a sentence naming the unmet precondition, together with the file that serves the domain. This is a text scan of those two directories. It is enough to stop a new component from claiming a name that an existing site on the same server answers for. Ingress is offered only for managed components, and a target the operator configured by hand is refused.

Two things are deliberately left unchecked at this stage. DNS may still point somewhere else, which is what allows the new server to be tested before any client is sent to it. And nothing probes the loopback port. If the component's container has not been deployed yet, Nginx answers 502 for the site, while the challenge path still works because Nginx serves it without the application. The documented order is to build and deploy the component first and add ingress afterwards, but the http stage does not enforce that.

04 / 10

Every Nginx write is tested and can be put back

The Nginx instance is shared, so a bad file written for one new site must never be left where the next reload or restart of every other site will find it. For that reason the stages that change Nginx, which are http, https and a fourth stage called remove, all go through the same procedure. The agent first takes a lock shared by every Hostwatch writer of that Nginx instance, so two changes cannot interleave. It reads the current managed file, if there is one, and keeps its bytes. If a file with that name exists but does not begin with the managed comment, the agent stops, because it will not replace a file it did not write. It then writes the new content to a temporary file in the same directory, syncs it to disk and renames it into place, so Nginx can never read half a file.

Next it runs nginx -t, which tests the complete configuration with the new file in it. If the test passes, it asks systemd to reload Nginx. If either the test or the reload fails, the agent restores what was there: it writes the previous bytes back, or deletes the new file if there was none before, then tests and reloads again. The error returned to the caller includes Nginx's own message and says whether the previous file was restored. Only after a successful reload does the agent record the new stage and domains in its own state. If that final record cannot be saved, the operation says so, because Nginx and the record now disagree and somebody should look.

The procedure has three limits. The test covers the whole configuration, which is the correct scope on a shared server, and it means a correct component file is rolled back when somebody else has left a broken file elsewhere in the tree. The Nginx message in the error names the real cause. The lock coordinates Hostwatch's own writers and does nothing about a person saving a file from an editor at the same moment. Restoring puts the previous file back, but it cannot repair a configuration that was already invalid before the change, and in that case the error reports that the restore itself failed.

05 / 10

Stage certificate refuses to run until DNS points at the server

The certificate stage has three preconditions of its own. The http stage must be installed, so that the challenge path is being served. The issuance unit, described in the next section, must be installed on the server. And every domain must resolve only to this server. For the last one the agent looks each name up through the server's own resolver, IPv4 and IPv6 records together, with a short timeout. It then collects the public addresses configured on the server's network interfaces. A domain passes when the lookup returns at least one address and every returned address is one of those. A single address that belongs elsewhere fails the domain, and so does a lookup that returns an error.

The word only matters because the authority chooses which address to connect to, and the operator has no say in it. Let's Encrypt prefers IPv6 when a name has both kinds of record, so an AAAA record left over from the previous host sends validation to the old machine while every IPv4 test from a laptop looks fine. Our article on TLS renewal covers that case in detail. A name with two A records, one old and one new, would validate on some attempts and fail on others. Requiring every address to be local removes these cases before any request leaves the server. It also protects the allowance for failed validations mentioned earlier. A refusal on the server costs nothing, arrives immediately and names the domain whose records need attention.

The check is narrower than it may sound. It reflects what the server's resolver returns at that moment, which is neither the authoritative answer nor what the authority will see a minute later, and a resolver cache can lag in either direction. A pass does not guarantee that validation succeeds, so the outcome is still read from the issuance record afterwards. Because the comparison is with addresses on the server's own interfaces, a domain that resolves to a CDN or proxy in front of the server does not pass, and neither does a server whose public address is translated by its provider and never appears on an interface. This stage does not issue certificates for those arrangements.

A glass prism sends two light conduits toward a foreground server miniature and an older miniature at the side, while a metal shutter in front of a glass capsule stays closed
EDITORIAL IMAGEOriginal AI-generated conceptual illustration, not a product screenshot or production measurement. The certificate stage stays blocked while any address for the domain leads to a different machine. Open full size ↗
06 / 10

A separate systemd unit runs Certbot

The agent does not run Certbot itself. Applying the certificate stage writes a small request file into a private directory, naming the component, the certificate name, the domains, the webroot and the managed Nginx file. It deletes any earlier result for that component and asks systemd to start one instance of a template unit, hostwatch-certbot-issue@ followed by the component id, without waiting for it to finish. The reason is separation of write access. As shipped, the agent's own unit sees the system read-only apart from a short list of directories, which includes the Nginx configuration directories and excludes the certificate store. The issuance unit is the mirror image: it may write Certbot's directories, the webroot and the request directory, and it cannot write Nginx configuration.

The script that the unit runs does not trust the request file. The component id must have the expected form. The managed Nginx file must exist and begin with the managed comment. The request must name the same component, the certificate name derived from it and the expected webroot. Each requested domain must be a valid host name and must appear on a server_name line of that managed file. The unit can therefore only ask for a certificate for names that the component's installed Nginx file already serves. It then runs Certbot in webroot mode, non-interactively, with a fixed certificate name, an ECDSA key and the ACME account that already exists on the server. Certbot writes the challenge file into the webroot and Nginx serves it, so no listener is stopped during issuance.

Issuance is asynchronous. When Certbot exits, the script writes a result record with one of two outcomes, issued or failed, the last lines of Certbot's output and the start and finish times. The read-only operation component_ingress_status returns that record together with the current DNS check and the names and expiry date of the certificate on disk, and the caller polls it until one of the outcomes appears. A started unit is a request in progress and should not be reported as a certificate. The installed stage stays http throughout, because issuance changes only the certificate store. The unit and its script are installed by an operator on servers that are meant to issue certificates. Where the unit file is missing, the stage is blocked and says so.

07 / 10

Stage https: switch only when the certificate covers every domain

The https stage has two preconditions: the http stage is installed, and a current certificate covers every domain. For the second, the agent reads the component's certificate from the certificate store and requires that it has not expired and that it is valid for each requested name. If the file is missing, expired or lacks a name, the stage is blocked with an instruction to run the certificate stage and wait for it. This check reads the dates and names of the certificate itself. Whether Nginx can load the certificate and its key is established by the configuration test that follows the write.

When the stage is applied, the managed file is rewritten with two server blocks. The port 80 block keeps the challenge location and answers every other request with a permanent redirect, status 308, to the same host and path over HTTPS. The port 443 block serves TLS 1.2 and 1.3 with the component's certificate and key, keeps the challenge location as well, and proxies to the same loopback port with the same headers as before. Keeping the challenge path reachable on port 80 lets later renewals validate the same way the first issuance did. The write goes through the procedure from the earlier section, so if the test or the reload fails, the http-stage file comes back and the site continues over plain HTTP.

After that, renewal belongs to Certbot, whose own timer renews the certificate. The issuance command sets no deploy hook, so whether Nginx reloads after a renewal depends on the hooks already installed on that server. It is worth confirming once, and our TLS renewal article explains how to check that clients receive the renewed certificate. The generated blocks are also a fixed template. They add no Strict-Transport-Security header and no WebSocket upgrade headers, and because each stage rewrites the whole file, edits made by hand do not survive the next stage.

08 / 10

Preview each stage and read the blockers

Every stage is two calls. The read-only component_ingress_preview returns the exact Nginx file that would be written, the DNS check for each domain with the addresses found, the certificate status, the list of blockers, a ready flag and a revision. The revision is a hash over the component, the stage, the domains, the currently installed stage, the rendered file, the blockers and whether the certificate covers the domains. The write, component_ingress, must carry that revision. The agent computes the preview again at the moment of the write and compares. If any of those inputs changed in between, for example a DNS record that now produces a blocker or a certificate that appeared, the revisions differ and the write is refused with a request to preview again. All of these operations are limited to the owner role in the workspace. The write also needs write scope and an explicit confirmation, and the server's MCP governance policy can still deny it.

Blockers are written to be acted on. One says that a domain is already served by a named file. Another says that a domain does not resolve only to this server yet and that its A and AAAA records should be updated first. Others say that no current certificate covers every domain, or that the issuance unit is not installed. Each one points at who has to do something: the operator at the DNS provider, the operator on the server, or nobody, because the answer is to wait. An assistant working through MCP should report the blocker and stop there. Changing the list of domains later is handled the same way. Every stage except http is blocked until http has been applied with the new list, which means walking through the stages again.

The component comes from an earlier step. The operations component_preview and component_create derive a new target from a template that the operator configured on the same server, without inheriting its secrets, data or domains. A separate article describes that process. For this one it is enough to know that a managed component has a fixed loopback port and that ingress is available only for such components.

09 / 10

What the operator still decides

Hostwatch does not edit DNS. The domain, the zone it lives in, its A and AAAA records and their lifetimes are changed at the DNS provider by whoever owns them. That includes removing records that should no longer exist, such as an AAAA record that pointed at a previous host. Our article on DNS cutovers explains how to lower a record's lifetime ahead of a move and how to compare authoritative answers with what resolvers return, so that is not repeated here. The server also has to be prepared by an operator before any stage works: an agent version that includes the component operations, Nginx, Certbot with a registered ACME account, the shared webroot, the issuance unit with its script, and a template for components.

The time of the cutover is also the operator's decision, and the staged design makes its cost visible. Between the DNS change and the https stage, the site is served over plain HTTP only. On a shared Nginx that already serves other sites over HTTPS, a request to the new domain on port 443 during that period is typically answered by the default TLS server block for the port, with a certificate for a different name, and the browser shows a warning. For a new domain that nobody visits yet this is harmless. For a site moving from a host where it already had HTTPS, it is an interruption that users see, and it lasts until issuance has finished and the https stage is applied. This operation issues only through the HTTP challenge, so it cannot obtain the certificate before DNS points at the server.

The practical plan for such a move is to install and test the http stage in advance, change DNS at a quiet hour, start the certificate stage as soon as the preview stops reporting the DNS blocker, and apply https as soon as the status shows the certificate. Taking a site down is a decision too. The remove stage deletes the managed file with the same test and restore as the other stages. It leaves the certificate in place, neither deleted nor revoked, and a component can be forgotten only after its ingress and its container are gone.

A lever switch on a stone plinth routes one light conduit between an older and a newer server miniature, with a glass lens on a stand observing the junction
EDITORIAL IMAGEOriginal AI-generated conceptual illustration, not a product screenshot or production measurement. The operator chooses when the DNS change happens, and the stages on the server wait for it. Open full size ↗
10 / 10

Verify each stage from outside the server

After the http stage and before the DNS change, send a request to the server's public address while keeping the domain as the host name. The resolve option of curl does this, and the DNS cutover article shows the form. The expected answer is the application's own page. A 502 means Nginx matched the domain and found nothing on the loopback port, and a page from a different site means the request did not match the new server block. Then request a made-up file name under the challenge path. A plain 404 from Nginx itself shows that the path is handled before the proxy. Once DNS has changed, repeat the first request without directing the address, from a network outside the server, over IPv4 and IPv6 separately if both are published. Hostwatch does not test from outside whether port 80 is reachable, so a firewall in front of the server shows up only as a failed validation.

After the certificate stage nothing changes for an outside client, because port 443 is still not configured for the domain. The evidence is in component_ingress_status: the outcome issued, certificate names that match the domain list, and an expiry date. If the outcome is failed, read the last lines of Certbot's output in the record and correct the cause before starting the stage again. A leftover DNS record and a closed port 80 are common causes, and each new attempt is a real request to the authority.

After the https stage, make an ordinary HTTPS request with the real host name and with certificate verification left on. Look at the certificate the client received and compare its names and expiry with the status. Request the plain HTTP address and confirm that the redirect keeps the host and path. Finish with one representative request to the application, because a valid certificate in front of a failing application is still a failed launch. The TLS renewal article describes how to inspect the served certificate. Write down what was checked and from where, and leave open anything that could not be tested, such as IPv6 from a network that has none. Hostwatch's checks all run on the server, and none of them observes what a client on another network receives.

THE TAKEAWAY

Install the HTTP proxy first, request the certificate only when every address for the domain belongs to the server, and add the HTTPS block only when the certificate covers every domain. Test the whole Nginx configuration on each write, keep the previous file ready to restore, and confirm the result from outside.

Further reading

UP NEXT / Service operationsA new site through MCP starts from a template the operator already trusts