Write down the route you expect
This walkthrough assumes a Linux VPS you administer, a working SSH session, an application with a harmless /healthz endpoint, and access to the authoritative DNS settings for a domain you control. The example app listens on 127.0.0.1:3000; Caddy is the public reverse proxy. If your app uses a different supervisor or proxy, keep the diagnostic order and use its documentation.
api.example.com and 203.0.113.10 are documentation placeholders, not a live service. Replace both before running checks against your own system. Note the real hostname, intended IP address, app port and service name in one place. Setting a VPS hostname in the configurator does not create a public DNS record.
- DNS returns the intended address.
- The connection reaches the intended server and port.
- TLS authenticates the requested hostname.
- The proxy forwards the request to the right upstream.
- The application returns the expected response.
Check both address families
From a machine outside the VPS, query the records you intend to publish. dig belongs to BIND's DNS tools; package names vary. These commands request the answer section so you can see record type, address and remaining cache lifetime. See the BIND dig reference.
dig api.example.com A +noall +answer
dig api.example.com AAAA +noall +answer
Compare every returned address with your intended destination. Publish an AAAA record only when IPv6 routing, listening and filtering work for that address. An old AAAA record can send some clients somewhere different from the A record. If you intentionally use a CDN or DNS proxy, its addresses may be correct; record that additional hop instead of assuming the VPS must appear.
A blank answer needs a closer look at the complete dig response: it can mean no record of that type, a nonexistent name or a resolution problem. Check which DNS service is authoritative before editing. Record the old value and TTL, make the intended change there, then compare fresh results after existing caches age out. Repeated unrelated edits make the timeline harder to understand.
Separate connection failures from certificate failures
Request the small endpoint from outside the server. Use GET rather than assuming your application implements HEAD. Curl's timeout options bound the check; its verbose output shows connection and TLS progress. Curl documents these options and certificate verification.
curl --verbose --connect-timeout 5 --max-time 10 https://api.example.com/healthz
A resolution error points back to DNS. A refused connection means the connection was actively rejected; a timeout can involve routing or filtering and does not identify which firewall caused it. A certificate error means the expected secure connection was not established. Do not make disabling certificate checks your permanent fix.
To compare a specific origin while keeping the hostname in the TLS request, use curl's address override:
curl --verbose --connect-timeout 5 --max-time 10 --resolve api.example.com:443:203.0.113.10 https://api.example.com/healthz
If this succeeds while the ordinary request fails, compare DNS and any intermediary. This override does not edit DNS. When both A and AAAA records exist, repeat the ordinary request with --ipv4 and --ipv6 from a client that actually supports the corresponding network.
Inspect the server end of the connection
On the VPS, inspect listening TCP sockets and query the app directly:
sudo ss -ltnp
curl --silent --show-error --max-time 5 http://127.0.0.1:3000/healthz
ss shows listeners and, with sufficient permissions, their processes. A loopback listener is reachable locally; its presence alone says nothing about external access. Consult the upstream ss manual. If the direct app request fails, move to the process diagnosis guide before changing DNS.
For this single-app layout, the relevant Caddyfile block is:
api.example.com {
reverse_proxy 127.0.0.1:3000
}
The hostname and upstream must match your application. Caddy's reverse proxy directive forwards requests to the configured upstream; putting an arbitrary domain here does not give you control of it. Preserve the existing configuration before editing. With the packaged service and this file path, validate first, then reload only after validation succeeds:
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl reload caddy
The commands have different purposes: validation checks configuration loading, while reload applies the change. Check the installed service's actual file path and permissions. Caddy's command reference explains validation and reload behavior.
Verify the complete path, then keep the evidence
For ordinary public certificate automation, Caddy needs correct DNS, externally reachable challenge ports, permission to bind its listeners, and persistent writable certificate storage. The HTTP and TLS-ALPN challenges use ports 80 and 443 respectively; a DNS challenge is a separate setup. See Caddy's HTTPS prerequisites. Keep administrative access intact when reviewing firewall rules.
Illustrative result: the local endpoint returns {"status":"ok"}, the external HTTPS request returns the same small body, and curl reports successful certificate verification. These are expected observations for this example, not recorded results from an OffVPS server. Also exercise one normal application action: a shallow health endpoint may pass while a database-dependent route fails.
Record the request time, hostname, address family and first failing layer. That brief is more useful than “the domain is broken.” Once the path works, use the repeatable release guide to make the same checks part of every deployment.
Documentation used
Primary references for this page. Check the documentation for the version installed in your own environment.