← blog
A Practical VPS Setup for a Static Portfolio

A Practical VPS Setup for a Static Portfolio

A small, production-minded setup: static Astro files served by nginx, HTTPS and security headers, cache rules, PM2 and one optional API service.

by Danila— views

Self-hosting a static portfolio is not automatically better than a managed platform. It trades convenience for control and turns a small website into a place to practise DNS, TLS, nginx, caching and routine server maintenance. That can be valuable when learning is part of the goal; it is unnecessary overhead when the only goal is to publish quickly.

The setup below is a deliberately small example. The domain, ports and application are fictional, while the configuration patterns come from the linked primary documentation.

A small production path

Suppose portfolio.example is an Astro site with one optional view counter. Ordinary pages do not need a Node process:

browser ──> nginx ──> dist/
             └─────> /api/views ──> Node on 127.0.0.1:4602

nginx serves the generated HTML, CSS, JavaScript, fonts and images directly. Only /api/views reaches the local service. If the counter stops, articles should still load; a decorative metric is not allowed to become a page outage.

An Astro preview process may run separately on 127.0.0.1:4321, but it is not in the production request path. Keeping preview and production distinct prevents a routine process restart from becoming a public incident.

Build once and serve files

The deployment can be as small as:

npm ci
npm run build

The build writes static output to dist/, and nginx points its root there. A successful content build replaces files without restarting an application server. This is the main reliability advantage: no database connection, server render or Node process is required to open an article.

Atomic deployment is safer than writing into the live directory file by file. A production variation can build into a versioned directory, verify it and switch a symlink only after every check passes. Keep the previous directory for a quick rollback.

Choose one canonical host and enforce HTTPS

Redirect plain HTTP to HTTPS and choose either the apex domain or www as canonical. The other HTTPS host should redirect too. This avoids multiple public versions of each URL and keeps canonical metadata, cookies and analytics predictable.

Let’s Encrypt uses ACME to verify domain control and recommends Certbot as a starting client. Certificate renewal needs a real test; a timer that looks enabled is not proof that the next challenge will succeed.

HTTP/2 over TLS is a sensible baseline. HTTP/3 can be added when the installed nginx build supports it and measurement justifies the change. A new protocol is not a substitute for correct caching or a fast response.

Cache by URL stability

A practical split looks like this:

One site-wide duration is easier to write but harder to operate. A year is safe only when changing the content also changes the URL. The related browser-side choices are covered in how to keep an Astro site fast.

Security headers are a policy, not a badge

A production response commonly needs HSTS, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, Permissions-Policy and framing restrictions. OWASP’s security-header testing guide stresses that presence alone is not enough. Duplicated, conflicting or overly broad values can undermine the intended policy.

A CSP can start with default-src 'self' and open only the sources the page genuinely uses. Deploy a new policy in report-only mode first when breaking existing scripts would be costly. If inline scripts require 'unsafe-inline', document that exception rather than calling the result strict.

HSTS should only include includeSubDomains and preload when every current and future subdomain is ready for HTTPS. MDN’s HSTS reference explains the preload requirements and why reversing the decision is slow.

Keep the dynamic service narrow

In the fictional view-counter example, the Node service listens only on loopback and receives traffic through nginx. The public firewall does not expose its port. The API accepts known slugs, caps request bodies and rejects everything else.

PM2 or another supervisor can restart the service after an exit, but restart loops still need logs and limits. Store writes should be atomic, and a counter that can lose data under concurrent increments needs a real database or another safe storage strategy before traffic grows.

Most importantly, design the page to degrade: if the API times out, show no count rather than blocking the article.

A deployment check that follows the request

After a content change, verify the same route a visitor will use:

npm run build
curl -I https://portfolio.example/
curl -I https://portfolio.example/_astro/<hashed-file>.css
curl https://portfolio.example/sitemap-index.xml

Check the status, canonical redirects, cache headers, security headers and localized pages. Restart PM2 only when the service or its configuration changed. Reload nginx only after nginx -t succeeds. Habitual restarts add risk without proving anything.

When managed hosting is the better answer

Choose a managed static host when preview deployments, instant rollback, global delivery and reduced maintenance matter more than server control. For many portfolios, that is the rational choice.

Choose a VPS when operating the server is an intentional part of the project and there is time to patch it, monitor it and practise recovery. The useful lesson is not that self-hosting is universally cheaper or faster. It is that every extra layer should have a clear job and a failure mode the site can survive.