A typical full-stack project has three parts: a front end (React, Vue, Svelte or similar), an API (Node, Python, Go, PHP…) and a database. On one VPS you can run all three reliably with Docker Compose behind Nginx, and deploy every push automatically.

The target setup

https://example.com       → Nginx → static front-end files
https://example.com/api/  → Nginx → API container (127.0.0.1:4000)
                                    └─ PostgreSQL container (internal network only)

1. Prepare the server

Follow a basic hardening checklist first (Ubuntu or Debian), then install Docker and Nginx:

curl -fsSL https://get.docker.com | sh
apt install -y nginx certbot python3-certbot-nginx
adduser deploy && usermod -aG docker deploy
mkdir -p /srv/app && chown deploy: /srv/app

2. Containerize the API

api/Dockerfile (Node example):

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 4000
CMD ["node", "server.js"]

3. Compose file on the server

/srv/app/compose.yaml:

services:
  api:
    image: ghcr.io/you/app-api:${TAG:-latest}
    restart: unless-stopped
    env_file: .env
    ports:
      - "127.0.0.1:4000:4000"
    depends_on: [db]

  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

/srv/app/.env (permissions 600, never committed):

DB_PASSWORD=change-me
DATABASE_URL=postgres://app:change-me@db:5432/app
JWT_SECRET=…

The database has no published port — only the API container can reach it.

4. Nginx: static front end + API

Build the front end into static files (npm run build → dist/) and serve them from /srv/app/web:

server {
    server_name example.com;
    root /srv/app/web;

    location /api/ {
        proxy_pass http://127.0.0.1:4000/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location / {
        try_files $uri /index.html;     # single-page app routing
    }
}
certbot --nginx -d example.com --redirect

5. Automate deploys with GitHub Actions

.github/workflows/deploy.yml:

name: deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions: { contents: read, packages: write }
    steps:
      - uses: actions/checkout@v4

      - name: Build and push API image
        run: |
          echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
          docker build -t ghcr.io/${{ github.repository }}-api:${{ github.sha }} api
          docker push ghcr.io/${{ github.repository }}-api:${{ github.sha }}

      - name: Build front end
        run: cd web && npm ci && npm run build

      - name: Upload front end
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.HOST }}
          username: deploy
          key: ${{ secrets.SSH_KEY }}
          source: web/dist/*
          target: /srv/app/web
          strip_components: 2

      - name: Restart API
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.HOST }}
          username: deploy
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /srv/app
            export TAG=${{ github.sha }}
            docker compose pull api
            docker compose run --rm api npm run migrate
            docker compose up -d api
            docker image prune -f

(Pin action versions to ones you’ve reviewed; the image name must be lowercase.)

6. Rollback

Because images are tagged by commit, rolling back is one command:

cd /srv/app && TAG=<previous-commit-sha> docker compose up -d api

Keep database migrations backward-compatible (add columns before removing old ones) so rollbacks don’t break.

7. Backups and monitoring

  • Dump PostgreSQL nightly (docker compose exec db pg_dump -U app app) and ship it off-server — see backup to cloud storage.
  • Add uptime checks for / and /api/health.

Summary

Containers for the API and database, Nginx for TLS and static files, environment variables outside Git, and a CI job that builds, migrates and restarts. A 2–4 GB Docker VPS runs this comfortably for most projects. Related: Nginx reverse proxy for multiple containers and PostgreSQL on a VPS.

  • Deployment
  • Docker
  • CI/CD