My main site ran on Vercel at the apex of my domain. A self-hosted copy of the same Next.js site ran on my VPS at mirror.example.com, marked noindex so it would not compete with the real one. I decided to make the VPS copy the primary site.
The move took four steps: rebuild the image as indexable, add an nginx vhost for the apex, change one A record, and issue a certificate. The step that cost me time was the first one. The noindex flag was baked into the image, and changing the container’s environment did not remove it.
Where I started
One container named portfolio runs on the VPS, bound to 127.0.0.1:3010. The host nginx terminates TLS and proxies to it.
The repository was built to be a mirror. A build-time variable, NEXT_PUBLIC_MIRROR=1, switched on three things:
- an
X-Robots-Tag: noindex,nofollowresponse header, set innext.config.ts - a
noindexrobots meta tag, set in the root layout’s metadata - a
robots.txtthat disallows everything
That is the right setup for a mirror. For the primary site, all three had to go.
Step 1: rebuild the image as indexable
My first idea was to change NEXT_PUBLIC_MIRROR in the compose file and restart the container. That does nothing.
Next.js inlines NEXT_PUBLIC_ variables into the app when it is built. The header, the meta tag and the robots file had all been decided at build time. The running container never reads the variable again.
The real fix was in two places:
- the Dockerfile default for the build argument, changed from
1to0 - the build argument passed by the CI workflow, changed the same way
Pushing that commit triggered the deploy workflow. It rebuilt the image and restarted the container. Later pushes stay indexable because the default itself changed.
The compose file on the server still has a runtime NEXT_PUBLIC_MIRROR: 1 line. It is stale and has no effect. I mention it because it looks alarming when you read the file later.
If you take one thing from this post: for a NEXT_PUBLIC_ value, a restart is not enough. Check the built output, not the environment.
Step 2: add an nginx vhost for the apex
The mirror hostname already had its own vhost. I added a second one for the apex and www, pointing at the same container.
This is the vhost template I keep for sites like this, with the hostname replaced. It is the copy from this blog’s repository, so the port is the blog’s. The portfolio vhost proxies to 127.0.0.1:3010.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:3011;
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;
}
}
Save it in /etc/nginx/sites-available/, symlink it into sites-enabled, then test and reload:
nginx -t && systemctl reload nginx
The file only has a port 80 block. Certbot adds the 443 block and the redirect in step 4.
The important difference from the mirror vhost is what this one leaves out. It has no X-Robots-Tag header. The template carries a comment saying so, because copying the mirror’s vhost and forgetting to delete that line would quietly deindex the main site.
Step 3: change the A record
DNS for the domain is at Hostinger, not at Vercel. The apex @ record was an A record pointing at Vercel’s address, 76.76.21.21. I changed it to the VPS address, 203.0.113.10 in this post.
www is a CNAME to the apex, so it followed without a change.
Rollback is the same edit in reverse. I left the Vercel project in place. It no longer receives traffic for the apex, but setting the A record back would restore the old site.
Step 4: issue the certificate
certbot --nginx -d example.com -d www.example.com --redirect
--nginx lets certbot edit the vhost from step 2. It adds the port 443 server block with the certificate paths. --redirect adds the HTTP to HTTPS redirect, which is a 301.
The server already had a certbot account from other sites, so I did not need to pass an email address.
The order matters. Certbot proves control of the hostname over HTTP, so the A record must already point at the VPS and the port 80 vhost must already be live. That is why DNS is step 3 and certbot is step 4.
The certificate was issued on 8 November 2026 and expires on 5 February 2027. Certbot renews it automatically. The options are described in the certbot documentation.
Keeping the mirror out of Google
After step 1 the image is indexable. The mirror hostname still serves the same container, so by default it would now be indexable as well, and I would have the same content on two hostnames.
I kept the mirror noindex at the nginx layer. Its vhost adds an X-Robots-Tag: noindex,nofollow header to every response. The app no longer sets the header, but nginx does, and only for that hostname.
The result:
| Hostname | Served by | X-Robots-Tag from nginx |
|---|---|---|
example.com, www.example.com | portfolio container | none |
mirror.example.com | same container | noindex,nofollow |
Both hostnames serve identical content from one container. Only the header differs.
A cleaner end state is a 301 from the mirror hostname to the apex, which removes the duplicate entirely. I have not done that yet.
Checking the result
I checked the new server by sending requests for the real hostname straight to the VPS address:
curl -sS -I --resolve example.com:443:203.0.113.10 https://example.com
--resolve makes curl skip DNS and connect to the address you give it. With that I confirmed:
- the apex returns
200and the response comes from nginx robots.txtallows crawling and lists the sitemap- the page has a robots meta tag of
index,follow http://redirects tohttps://with a301wwwreturns200
One warning about this check. It proves the server is right. It does not prove DNS is right, because it never asks DNS. Run at least one request without --resolve before you call the move done.
What I would repeat
- Decide which layer owns
noindex. For a build that must be indexable on one hostname and hidden on another, the proxy is the right place. The app cannot tell the two apart as easily as nginx can. - Treat
NEXT_PUBLIC_values as part of the image. Changing one means a rebuild. - Put the vhost in place before touching DNS, and run certbot after.
- Keep the old host’s project until you are sure. It is the cheapest rollback there is.
- Each new hostname needs its own
Arecord. My zone has no wildcard record, so the mirror hostname did not resolve until I added one for it, and certbot could not issue its certificate before that.