DEV Community

Sanket Jagtap
Sanket Jagtap

Posted on • Originally published at sanket-jagtap.in on

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

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)