Skip to main content
Hosting Tips

Deploy a Local App to a VPS with Claude Code: DNS and HTTPS

6 min readSharpHeberg
Deploy a Local App to a VPS with Claude Code: DNS and HTTPS

Your app works on localhost. Now you want to share it through a real domain, without leaving your laptop running. The missing pieces are a VPS, a managed application process, DNS and an HTTPS reverse proxy.

This guide shows that path with a small Node.js app. Claude Code can help inspect your project and prepare configuration for review; you remain responsible for the server changes. The model is not installed on the VPS, and Claude access and billing are separate from hosting.

From localhost to a public VPS: an illustrated deployment walkthrough
Animated walkthrough. The dashboard is a fictional demonstration, not live infrastructure monitoring.

Watch the deployment overview

What we actually tested: a real dashboard originally created by Claude Code was deployed by our operator to a disposable Linux VPS. We verified public DNS, trusted HTTPS, HTTP-to-HTTPS redirection, non-root application and proxy processes, and access after restarting the application service. There was no new autonomous Claude deployment run. The suggested prompt below is instructional, not a recorded response.

Before you start

  • A Linux VPS with systemd and an SSH account with sudo access. Use a test VPS first; do not overwrite an existing site’s services or web configuration.
  • A supported Node.js LTS runtime installed for the server, and Caddy installed as a system service. Install these prerequisites before following the deployment steps.
  • A domain or subdomain whose DNS records you can edit.
  • A small, self-contained app. Our example serves index.html using server.mjs, with no package dependencies or database.

Framework applications need their own build command, start command, dependencies and storage plan. Do not assume a Next.js, Python or database-backed project can use this two-file example unchanged.

1. Ask Claude Code to inspect the project before changing it

Start in your local project directory. A useful first request is:

code
Inspect this project without changing files. Identify its runtime,
build and start commands, configuration and secrets. Propose a Linux
VPS deployment using a non-root systemd service and Caddy HTTPS.
Bind the app to 127.0.0.1:3000. Explain each proposed file and
verification step. Do not connect to a server or edit DNS.

Claude Code’s documented workflows include planning before editing. Review the proposed changes rather than accepting deployment commands blindly. Keep API keys, production credentials and private SSH keys out of prompts and repositories; read-only access does not make sensitive files confidential.

2. Prepare and transfer the application

For this exact minimal example, save your HTML as index.html and use the following server. It binds to loopback by default and does not expose arbitrary project files:

code
import {createServer} from 'node:http';
import {readFileSync} from 'node:fs';
import {fileURLToPath} from 'node:url';
const page=readFileSync(new URL('./index.html',import.meta.url));
export function createAppServer(){
 return createServer((req,res)=>{
  const pathname=new URL(req.url,'http://localhost').pathname;
  if(pathname!=='/' && pathname!=='/index.html'){
   res.writeHead(404,{'Content-Type':'text/plain; charset=utf-8'});res.end('Not found');return;
  }
  res.writeHead(200,{'Content-Type':'text/html; charset=utf-8','X-Content-Type-Options':'nosniff'});res.end(page);
 });
}
if(process.argv[1]===fileURLToPath(import.meta.url)){
 const host=process.env.HOST||'127.0.0.1';const port=Number(process.env.PORT||3000);
 const server=createAppServer();server.listen(port,host,()=>console.log(`Demo app listening on ${host}:${port}`));
 process.on('SIGTERM',()=>server.close(()=>process.exit(0)));
}

Run it locally with node server.mjs, open http://127.0.0.1:3000, then stop it before continuing. On a VPS that already uses port 3000, choose another private port consistently in both the service and proxy.

From your local project directory, replace deploy and VPS_IP with your SSH account and actual server address:

code
ssh deploy@VPS_IP 'mkdir -p ~/my-app'
scp index.html server.mjs deploy@VPS_IP:~/my-app/

On the server, install these two non-secret files into a readable application directory:

code
sudo install -d -m 0755 /opt/my-app
sudo install -m 0644 ~/my-app/index.html ~/my-app/server.mjs /opt/my-app/
command -v node

Record the absolute Node executable path. It must be accessible outside your login shell and home directory. A user-only version-manager installation under /home is not suitable for the hardened service below. Never copy a development .env, private key or database dump into a public web directory.

3. Run the app as a non-root systemd service

Use sudoedit /etc/systemd/system/my-app.service. Replace /ABSOLUTE/PATH/TO/node with the real server-wide executable path you just checked:

code
[Unit]
Description=My Node web app
After=network.target

[Service]
Type=simple
DynamicUser=true
WorkingDirectory=/opt/my-app
ExecStart=/ABSOLUTE/PATH/TO/node /opt/my-app/server.mjs
Environment=NODE_ENV=production
Environment=HOST=127.0.0.1
Environment=PORT=3000
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true

[Install]
WantedBy=multi-user.target

systemd’s execution controls document the dynamic account and hardening settings. This profile suits our read-only app; apps that write uploads or databases require deliberately configured persistent storage and permissions.

code
sudo systemctl daemon-reload
sudo systemctl enable --now my-app.service
sudo systemctl status my-app.service --no-pager
curl -fsS http://127.0.0.1:3000/

Resolve application errors before touching DNS. A service that is enabled is configured for boot-time startup, but that is not evidence of a completed reboot test.

4. Point a subdomain at the VPS

Create an A record for your chosen hostname, such as app.example.com, targeting your VPS’s public IPv4 address. With Cloudflare, our direct-server demonstration used DNS only and automatic TTL. Do not copy our demonstration IP into your own site’s records.

Check the public answer with dig +short A app.example.com. Inspect dig +short AAAA app.example.com too: if an IPv6 record exists, it must reach the same working deployment over IPv6. DNS only maps an address; it neither starts your app nor configures HTTPS.

5. Put Caddy in front of the private app

Keep the Node service on 127.0.0.1:3000. Make the public web ports 80 and 443 reachable through the provider firewall and host firewall, without removing your SSH access. Do not expose port 3000 to the internet.

Add this site block to your installed Caddy service’s configuration, replacing the example hostname. Preserve unrelated site blocks:

code
app.example.com {
    encode zstd gzip
    reverse_proxy 127.0.0.1:3000
}

The reverse_proxy directive forwards public requests to the private application. With a qualifying public hostname and working certificate-validation conditions, Caddy’s automatic HTTPS obtains and renews certificates and redirects HTTP to HTTPS.

For the standard Caddy system service and configuration path:

code
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl reload caddy

Our filming VPS used a dedicated Caddy service and isolated configuration paths rather than the standard shared service above. The routing principle is the same; these reader-facing paths and service names are adaptations, not a verbatim installation transcript.

6. Verify the public result and a service restart

code
curl -I http://app.example.com/
curl -I https://app.example.com/
sudo systemctl restart my-app.service
curl --retry 5 --retry-delay 1 --retry-connrefused -I https://app.example.com/

Check for an HTTP redirect to HTTPS, a successful HTTPS response and a valid browser certificate. Do not use curl -k to conceal trust errors. Open the page from another network, not just from inside the VPS.

A failed restart check needs investigation: inspect journalctl -u my-app.service -n 50 --no-pager and Caddy’s service logs. DNS errors, incorrect IPv6, a stopped app, a wrong upstream port and blocked certificate validation are different problems.

We verified an application-service restart, not a VPS reboot or a high-availability setup. Before hosting a real business application, also plan updates, backups, monitoring, secret management and any persistent data.

From local project to a public address

The complete path is straightforward: prepare the project, transfer it to the VPS, keep it running as a managed non-root service, point DNS to the server and terminate public HTTPS at the reverse proxy. Claude Code can help prepare and explain these pieces; review and operational checks still matter.

Explore SharpHeberg Linux VPS plans when you are ready to bring a local project online. Hosting supplies the server environment, not an included Claude subscription or a locally hosted Claude model.

Related Articles

Continue exploring our knowledge base with these related articles

Published on October 28, 2026 by SharpHeberg

Thank you for reading our technical insights and tutorials

Back to Blog