June 22, 2026/3 min read/Admin

Deploying a Full-Stack App with Docker and Nginx on a VPS

How an Angular frontend and a Dockerized Node API live together behind Nginx on a single VPS — reverse proxying /api and /uploads, SSL, and persistent uploads.

dockernginxdevopsdeploymentvpsangularnodejs

You do not need Kubernetes to ship a real app. A single VPS with Nginx and Docker handles a surprising amount of traffic. Here is the setup that runs this portfolio.

The topology

  • Nginx is the front door, terminating SSL and serving the built Angular app as static files.
  • The Node + Prisma API runs in a Docker container on a private port (6000).
  • MySQL is the database; uploaded images live on a persistent host volume.

Nginx as the front door

One server block does three jobs: serve the SPA with a history-API fallback, reverse-proxy the API, and serve uploaded files. The detail that bites everyone is the trailing slash on proxy_pass:

location /api/ {
  proxy_pass http://localhost:6000;   # no trailing slash -> keeps /api prefix
}
location / {
  try_files $uri $uri/ /index.html;   # SPA fallback
}

With a trailing slash Nginx strips the matched prefix, so /api/users would arrive at the backend as /users and 404. Dropping the slash preserves the path.

Persistent uploads

Anything written inside a container is gone on the next deploy. Uploads must live on the host and be mounted in, with ownership that matches the container user:

docker run -d --restart unless-stopped \
  -p 6000:6000 --env-file .env \
  -v /opt/app/uploads:/opt/app/uploads \
  portfolio

SSL

Let's Encrypt via certbot gives free, auto-renewing certificates. Nginx redirects all HTTP to HTTPS, so the app is TLS-only.

Deploys

The flow is pleasantly boring: pull the latest code, docker build, stop the old container, run the new one. Because migrations run on startup and uploads live outside the container, a redeploy is safe and repeatable.

Gotchas worth remembering

  • proxy_pass trailing slash changes the forwarded path — choose deliberately.
  • A non-root container user cannot write to a root-owned host volume; fix the ownership.
  • An existing database needs its migrations baselined once (P3005) before migrate deploy will run.

Share this article

Help others discover this content.