Domain troubleshooting
VersionGate can report the engine as healthy while the hostname does not open in a browser. PM2 online, preflight DNS, and the dashboard status badge are not the same as public reachability.
What "working" actually means
| Signal | What it proves | What it does not prove |
|---|---|---|
| pm2 status online | versiongate-api and versiongate-worker are running | Nginx, DNS, or TLS are correct |
| Preflight DNS A | This VPS resolved PUBLIC_DOMAIN | Your laptop or ISP can resolve the same name |
| Systems Operational | Dashboard chrome rendered | Nothing — that badge is not a live health check |
| Certbot success | Let's Encrypt issued a cert and nginx listens on 443 | Every resolver has the A record, or IPv6 is configured |
Split the problem into layers
Work from the process outward. Fix the first layer that fails before changing the next one.
| Layer | Test | Pass condition |
|---|---|---|
| 01 Engine | curl -sI --max-time 5 http://127.0.0.1:9090/ | HTTP from Bun on 9090 |
| 02 Nginx loopback | curl -sI --max-time 5 http://127.0.0.1/ | Nginx proxies to 9090 |
| 03 Host header | curl -sI --max-time 5 -H "Host: YOUR_DOMAIN" http://127.0.0.1/ | Same 200 for the real hostname |
| 04 TLS loopback | curl -sIk --max-time 5 --resolve YOUR_DOMAIN:443:127.0.0.1 https://YOUR_DOMAIN/ | Cert + 443 on this box |
| 05 Public ports | ss -lntp | grep -E ':80|:443|:9090' | nginx on 80/443, bun on 9090 |
| 06 DNS split | dig +short YOUR_DOMAIN A && dig +short YOUR_DOMAIN A @8.8.8.8 | Both return this server IPv4 |
Diagnostic block
Replace YOUR_DOMAIN with the hostname in PUBLIC_DOMAIN. Do not leave the placeholder — dig YOUR_DOMAIN looks up a name that does not exist.
DOMAIN=YOUR_DOMAIN grep -E 'PUBLIC_DOMAIN|PUBLIC_URL|NGINX_CONFIG_PATH|COOKIE_SECURE' ~/VersionGate/.env ss -lntp | grep -E ':80|:443|:9090' ls -la /etc/nginx/sites-enabled/ grep -RIn "server_name\|listen " /etc/nginx/sites-available /etc/nginx/sites-enabled /etc/nginx/conf.d dig +short "$DOMAIN" A dig +short "$DOMAIN" A @8.8.8.8 curl -sI --max-time 5 http://127.0.0.1/ curl -sI --max-time 5 -H "Host: $DOMAIN" http://127.0.0.1/ curl -sIk --max-time 5 --resolve "$DOMAIN:443:127.0.0.1" "https://$DOMAIN/"
curl http://$PUBLIC_IP/ often hangs. That is hairpin NAT: the guest cannot connect out to its own public address. Use loopback plus a Host header, or test 80/443 from a laptop. SSH working on a dedicated port does not by itself prove 80/443 are forwarded.DNS: server says yes, browser says no
Preflight and dig on the VPS use the server resolver. Your browser uses the laptop or router resolver. Those can disagree for hours after you add vg.henry.example.com as a nested subdomain.
nslookup YOUR_DOMAIN nslookup YOUR_DOMAIN 8.8.8.8
If the first command is NXDOMAIN and the second returns this server IPv4, the engine is fine. Point the laptop or router at 8.8.8.8 or 1.1.1.1, flush the local cache, and open https://YOUR_DOMAIN.
On Cloudflare: A record to the VPS IPv4, DNS only (grey cloud) until HTTP-01 succeeds. A parent name such as henry.example.com with no A record, or a broken CNAME, makes vg.henry.example.com fail on some resolvers. Do not add AAAA unless nginx also listens on [::]:443.
Three nginx files that fight each other
After install, Settings → Write nginx, Certbot, and a provider OS reset, these paths often all claim port 80:
| Path | Who writes it | Keep? |
|---|---|---|
| /etc/nginx/sites-available/versiongate | install.sh — often still server_name $PUBLIC_IP | Disable the sites-enabled symlink once conf.d has the TLS vhost |
| /etc/nginx/conf.d/versiongate.conf | Setup wizard + certbot --nginx | Yes — this is the live dashboard vhost |
| /etc/nginx/conf.d/upstream.conf | Settings Write nginx (NGINX_CONFIG_PATH) and TrafficService | upstream { } only — never a server { } block |
Default NGINX_CONFIG_PATH is /etc/nginx/conf.d/upstream.conf. Settings → Write nginx config writes a server block there, then the next production deploy overwrites that file with an upstream block. The dashboard reports success; the installer vhost on the IP never changes.
cp -a /etc/nginx/conf.d /root/nginx-conf.d.bak.$(date +%Y%m%d-%H%M)
cp -a /etc/nginx/sites-available/versiongate /root/nginx-sites-versiongate.bak
rm -f /etc/nginx/sites-enabled/versiongate
cat > /etc/nginx/conf.d/upstream.conf <<'EOF'
# Written by VersionGate on production deploy/promote
# Do not put server { } blocks in this file.
EOF
nginx -t && systemctl reload nginxNGINX_CONFIG_PATH points at the real vhost (or the product writes site and upstream to different files). Removing the IP-only site makes http://SERVER_IP/ return 404; use the hostname over HTTPS.Shared port, dedicated port, OS reset
A host that started on a shared/NAT port and later received a dedicated public port still sits behind the provider hypervisor (often Proxmox). Two OS resets reinstall VersionGate and stack another copy of the three nginx files above. SSH on the dedicated port only proves that port is forwarded. Confirm 80 and 443 from a machine that is not the VPS.
curl -sI --max-time 10 http://SERVER_IP/ curl -sI --max-time 10 --resolve YOUR_DOMAIN:443:SERVER_IP https://YOUR_DOMAIN/
Dashboard domain vs app URL
The dashboard hostname is PUBLIC_DOMAIN. Deployed apps are not separate vhosts. Open them at https://YOUR_DOMAIN/p/<project>/<environment>. Custom app hostnames such as app.example.com are not written by the engine today.