Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Deploy the application as two processes: Nginx listens on the public HTTP and HTTPS ports, serves files that never need application logic, and forwards dynamic requests to a private application server. This guide uses Ubuntu 24.04, Nginx installed on the host, and an example Node.js 20 LTS application listening on 127.0.0.1:3000. The same boundary works with another runtime as long as it exposes an HTTP listener.

What the deployment looks like

Nginx is the public web-server and reverse-proxy layer, not the job-application runtime. A browser connects to Nginx; Nginx decides whether it can return a file directly or must pass the request to the application process. The application validates submissions, runs business rules, and talks to whatever data services your project requires.

Browser
   |
   | HTTPS :443 (HTTP :80 redirects)
   v
Nginx on the server
   |-- /assets/*, existing public files  -> filesystem
   |-- /api/*, /apply, /dashboard         -> 127.0.0.1:3000
   v
Node.js application server

NGINX documents this split and the use of an upstream referenced by proxy_pass in its Node.js deployment guide; Node.js is an example here, not a requirement for a job-application product. See NGINX’s web-server administration guide and its Node.js deployment guide.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Request Handler in this example Reason
/assets/app.css, images, browser bundles Nginx filesystem Lower overhead and no application worker is needed.
/apply and /api/applications Node.js on 127.0.0.1:3000 These routes need validation, sessions, persistence, or other application logic.
/healthz Application (or a deliberately static Nginx check) Provides a useful release and monitoring signal.

Choose the operating model before installing anything

The commands below assume a single Linux server, host-managed Nginx, and a host-managed Node.js service. That is the clearest starting point when the title does not specify a platform. Docker Compose is covered later as an optional route; do not combine both process managers accidentally.

  • A DNS record such as jobs.example.com points to the server’s public address.
  • Ports 80 and 443 are allowed through the server firewall and any cloud firewall.
  • The application has a production start command and listens on the address and port supplied by environment variables.
  • The deploy user can place releases under /var/www/job-app; Nginx can read the published files.
  • Application secrets are supplied through a protected environment file or service manager, not committed to the repository.

Do not assume that this web-server setup answers the domain-specific questions of candidate-data retention, upload storage and limits, email delivery, database backups, migrations, or jurisdiction-specific privacy obligations. Design and document those separately for the application you are deploying.

Make the application reachable only from the server

Use a production build and a private listener

Build the frontend and backend using the versions your project supports. Configure the process to bind to 127.0.0.1 (or a private container network address) rather than a public interface. A typical environment file might contain:

NODE_ENV=production
HOST=127.0.0.1
PORT=3000

Your framework may use different variable names; the important result is an HTTP listener on the address Nginx will proxy to. Verify locally before adding Nginx:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -i http://127.0.0.1:3000/healthz

Expect an HTTP response from the application. If this fails, fix the application process first; a reverse proxy cannot repair a process that is stopped, listening on another port, or crashing during startup.

Run the process under a service manager

For a host-managed deployment, create a dedicated unprivileged account and a systemd unit. Adjust ExecStart to your framework’s real production command:

sudo useradd --system --home /var/www/job-app --shell /usr/sbin/nologin jobapp
sudo mkdir -p /var/www/job-app/current
sudo chown -R jobapp:jobapp /var/www/job-app
sudo nano /etc/systemd/system/jobapp.service
[Unit]
Description=Job application web service
After=network.target

[Service]
Type=simple
User=jobapp
Group=jobapp
WorkingDirectory=/var/www/job-app/current
EnvironmentFile=/etc/jobapp/jobapp.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo mkdir -p /etc/jobapp
sudo chmod 750 /etc/jobapp
sudo chmod 640 /etc/jobapp/jobapp.env
sudo systemctl daemon-reload
sudo systemctl enable --now jobapp
sudo systemctl status jobapp

Check service output with sudo journalctl -u jobapp -n 100 --no-pager. Keep the application port private; Nginx is the component that should be exposed publicly.

Install Nginx and define the routing boundary

Install and test the package

sudo apt update
sudo apt install nginx
sudo systemctl enable --now nginx
nginx -v

On many distributions the main configuration is /etc/nginx/nginx.conf, with virtual hosts in /etc/nginx/sites-available and enabled links in /etc/nginx/sites-enabled. Distribution layouts vary, so inspect the active file with sudo nginx -T before editing.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Create a server block

Place this example in /etc/nginx/sites-available/jobs.example.com. It serves existing files below /var/www/job-app/current/public and sends everything else to the application:

upstream job_app {
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    listen 80;
    listen [::]:80;
    server_name jobs.example.com;

    root /var/www/job-app/current/public;
    index index.html;

    location /assets/ {
        try_files $uri =404;
        access_log off;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    location / {
        try_files $uri $uri/ @job_app;
    }

    location @job_app {
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection "";
        proxy_pass http://job_app;
    }
}

The try_files directive lets Nginx return a real asset or page. A missing path falls through to the named location, where proxy_pass forwards the original URI to the Node.js service. If your frontend is a single-page application, replace the fallback behavior with the framework’s documented production entry file, while keeping API paths explicitly proxied when necessary.

sudo ln -s /etc/nginx/sites-available/jobs.example.com /etc/nginx/sites-enabled/jobs.example.com
sudo nginx -t
sudo systemctl reload nginx

Never skip nginx -t. NGINX’s Beginner’s Guide explains that a reload makes the master process check the new configuration and attempt to apply it; if applying the new configuration fails, Nginx keeps serving with the old configuration. This makes validation and reload a safe deployment operation when used deliberately: NGINX Beginner’s Guide.

Add HTTPS without exposing the private key

Use a certificate and key readable by the Nginx master

Obtain a certificate for the exact hostname and install the public certificate and private key in protected locations. A common arrangement is:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
sudo install -d -m 750 /etc/nginx/tls
sudo install -m 644 fullchain.pem /etc/nginx/tls/jobs.example.com.crt
sudo install -m 600 privkey.pem /etc/nginx/tls/jobs.example.com.key

The certificate is public; the private key is not. Restrict key-file access and confirm that the Nginx master process can read it. Do not make the key world-readable merely to resolve a permission error. The official NGINX HTTPS configuration guide documents this distinction and shows the ssl_certificate and ssl_certificate_key directives.

Redirect HTTP and listen on 443

Replace the HTTP server block with a redirect and add a TLS server block (retain your existing locations):

server {
    listen 80;
    listen [::]:80;
    server_name jobs.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name jobs.example.com;

    ssl_certificate     /etc/nginx/tls/jobs.example.com.crt;
    ssl_certificate_key /etc/nginx/tls/jobs.example.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;

    root /var/www/job-app/current/public;
    index index.html;

    location /assets/ {
        try_files $uri =404;
        access_log off;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    location / {
        try_files $uri $uri/ @job_app;
    }

    location @job_app {
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection "";
        proxy_pass http://job_app;
    }
}

The guide lists TLSv1.2 TLSv1.3 and a high-strength cipher policy among its defaults, while noting that defaults have changed over time. Treat the installed Nginx version as authoritative, review its effective configuration, and do not describe those defaults as permanent.

sudo nginx -t
sudo systemctl reload nginx
curl -I https://jobs.example.com/

Release code with a rollback-friendly sequence

Use a versioned release directory and switch current only after the new code has been installed. A practical sequence is:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Copy the new application build to a new directory such as /var/www/job-app/releases/2026-10-18-01 and set ownership to jobapp:jobapp.
  2. Install production dependencies and build assets in that release, without placing secrets in the release directory.
  3. Run the new process or restart the service, then verify curl http://127.0.0.1:3000/healthz.
  4. Update the current symlink to the tested release and restart or reload the application service as required by the runtime.
  5. If Nginx configuration changed, run sudo nginx -t; only then run sudo systemctl reload nginx.
  6. Check the public HTTPS endpoint, application logs, and Nginx logs before declaring the release complete.

To roll back application code, point current to the previous known-good release and restart the service. To roll back a bad Nginx edit, restore the previous configuration and reload after another syntax test; a failed reload leaves the old configuration active.

Optional route: Docker Compose

Compose is convenient when you want the application, Nginx, and supporting services described as reproducible containers on one server. Docker calls single-server Compose deployment the easiest application deployment path and describes production overrides for settings such as code mounts, host ports, environment variables, restart policies, and logging. See Docker’s Compose production guide.

Keep the app port private

If Nginx runs on the host and the app runs in a container, bind the published port to loopback:

services:
  web:
    build: .
    environment:
      NODE_ENV: production
      HOST: 0.0.0.0
      PORT: 3000
    ports:
      - "127.0.0.1:3000:3000"
    restart: unless-stopped

If Nginx is another Compose service, put both services on a private Compose network and proxy to the service name instead of publishing the application port at all. Docker’s documentation states, “Publishing container ports is insecure by default.” A published port is available outside the host unless you bind it to a specific interface; binding to 127.0.0.1 or ::1 restricts access to the Docker host. Docker also notes a localhost-publishing caveat for releases older than 28.0.0, where same-layer-2 hosts may reach such ports, so account for the engine version and network controls: Docker port publishing and mapping.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Redeploy only the changed service

docker compose -f compose.yml -f compose.production.yml up -d --build web

docker compose ps
docker compose logs --tail=100 web

The production file can remove development bind mounts, set production environment variables, and add restart behavior. Rebuild and recreate the service when releasing code; do not assume that changing a source file on the host changes an already-built image. Keep database persistence, secret injection, migrations, backups, and health checks explicit rather than copying development defaults.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Host Nginx or containerized Nginx?

Choice Operational advantages Trade-offs
Host Nginx + host-managed app Simple systemd logs, direct certificate files, fewer moving parts. Package, runtime, and release updates are managed separately.
Host Nginx + Compose app Application dependencies are packaged; Nginx remains easy to inspect from the host. Port binding and container/host permissions must be designed correctly.
Compose Nginx + Compose app One deployment definition and private service-to-service networking. Certificate volumes, proxy logs, upgrades, and container health need a deliberate production design.

Neither arrangement is universally best. Choose the one your team can patch, observe, back up, and roll back reliably.

One application instance or several?

The configuration above has one upstream server. When the application is stateless and the server has capacity, you can list several private instances:

upstream job_app {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

Nginx will distribute requests among the upstream members. Before doing this, ensure sessions, uploaded files, caches, and other state are available consistently to every instance; otherwise users can see different behavior between requests. Multiple workers also require a release strategy that avoids taking every instance offline at once.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Verify the finished deployment

  • Configuration: sudo nginx -t exits successfully before every reload.
  • Process: systemctl is-active jobapp (or docker compose ps) reports the expected service as running.
  • Private path: the application answers on 127.0.0.1:3000, while an unintended public connection to port 3000 is blocked.
  • Static path: an asset returns from Nginx without an application error in the app log.
  • Dynamic path: the application form and API return the expected status through HTTPS.
  • Forwarded context: generated links and secure-cookie logic see the original host and HTTPS scheme through the forwarded headers.
  • Logs: inspect both Nginx error/access logs and the application journal or container logs for the same request.
  • Certificate: the hostname, chain, expiry, and private-key permissions are correct.

Common failures and their fixes

Symptom Likely cause Fix
502 Bad Gateway The app is stopped, listening on another address/port, or refusing connections. Check the service status and local curl; make the upstream match the actual listener.
404 for CSS or JavaScript Nginx root does not contain the production build, or the asset URL is wrong. Inspect the resolved current/public path and test the exact asset URL.
502 after a release The new process failed to start because of a missing environment variable or dependency. Read the service journal, restore the previous release, and fix startup before retrying.
Nginx will not reload A syntax, include, certificate-path, or permission error exists. Run sudo nginx -t; the old configuration remains active until a valid reload succeeds.
HTTPS redirects in a loop The app does not trust the proxy’s HTTPS indication and redirects an already secure request. Configure the framework’s trusted-proxy setting according to its documentation and retain X-Forwarded-Proto.
Application port is reachable from the internet A container port was published on all host interfaces or a firewall rule allows it. Bind it to loopback or a private network, remove unnecessary publishing, and audit firewall rules.

What this architecture does not decide

Nginx can terminate TLS, serve static content, and route requests, but it does not define how a job application stores candidate records, accepts and scans resumes, sends confirmation mail, performs database migrations, retains backups, or meets privacy law. Set those policies and controls at the application and data-service layers. Keep upload limits, timeout choices, authentication, rate limiting, and audit logging explicit; add them only after the application’s requirements and threat model are known.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.