Skip to content

Instantly share code, notes, and snippets.

@osamaqarem
Last active July 20, 2025 15:50
Show Gist options
  • Save osamaqarem/f7f19ccff04c6e9be88d2c4645bb395c to your computer and use it in GitHub Desktop.
Save osamaqarem/f7f19ccff04c6e9be88d2c4645bb395c to your computer and use it in GitHub Desktop.
Nginx on MacOS

Install nginx (Homebrew)

brew install nginx

Configuration file for nginx will be at /usr/local/etc/nginx/nginx.conf

Web apps can be stored at /usr/local/var/www

Commands

Start:

nginx

Stop:

nginx -s stop

Reload config:

nginx -s reload

Lint:

nginx -t

Firewall

  • Firewall was already disabled.
  • Port forward 80 & 443 in router settings.

Basic Reverse Proxy with Nginx

The full nginx.conf file will be at the end - this here is a basic proxy example.

In the block shown below - we created a web server:

1- 80 is the web server port. 2- localhost is where the web server is listening. 3- proxy_pass is the location we would like to proxy to. Should be the app server.

Therefore, this block is saying: proxy all requests at http://localhost:80 to http://localhost:3000.

server {
        listen 80;
        server_name localhost;
        location / {
            proxy_pass         http://192.168.100.190:3000; 
        }
    }

Issues

1. Dynamic IP

The webserver server_name will usually point to a domain name - which DNS records should be set up for so it resolves to our public IP address. However, if that IP is dynamic its a problem as it eventually changes.

In my case, I had a domain with namecheap and they allow you to update the IP address that a special DNS record points to with a GET request.

Therefore, we can automate that process with a bash script and a cronjob:

#!/usr/bin/env sh
IP4=$(dig @resolver1.opendns.com ANY myip.opendns.com +short)
echo "$IP4"
URL="https://dynamicdns.park-your-domain.com/update?host=%40&domain=mydomain.com&password=mypassword&ip=""${IP4}"
echo "$URL"
curl --request GET \
  --url $URL

Crontab:

  • List cronjobs:

crontab -l

crontab -e

2. SSL

Using Certbot (Let's Encrypt client) guide it is straightforward:

Install certbot:

brew install certbot

To only generate certificate:

sudo certbot certonly --standalone -d mydomain.com

To generate certificate and update nginx.conf file automatically:

sudo certbot --nginx

The location of the files generated will be at /etc/letsencrypt/archive. It also generates symbolic links to those files at /etc/letsencrypt/live.

To change our initial basic setup to use SSL and our domain name, this is how it would look like:

server {
    listen 443 ssl;
    server_name mydomain.com;

    ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem; # managed by Certbot

    location / {
        proxy_pass         http://localhost:3000; 
    }
}
  • "Managed by certbot" is a line added by certbot automatically after adjusting your nginx.conf file if you ran the automatic command. Otherwise you can amend your nginx.conf manually to include your SSL certificates.

  • One issue faced here is that the user process running nginx did not have the required permissions to read the certificates (error when running nginx -t). Grant the necessary permissions using chmod on the real SSL files at /etc/letsencrypt/archive/ekyc-demo.xyz/ and the symbolic ones at /etc/letsencrypt/live/ekyc-demo.xyz/.

Perform a comprehensive SSL test using SSL Labs.

Full Setup

  • 2 Apps on localhost 3000 and 5000.
  • Each with own subdomain and SSL certificates.
  • HTTP redirects to HTTPS.
#user  nobody;

# Number of processes should not exceed number of cores #
worker_processes  1;

# MINIMUM (probably too low): worker_connections * 2 file descriptors = 512 #
# No need to multiply by worker_prcocesses as the limit is applied to each worker #
# 1 descriptor for client connection, 1 for proxied server #
# Could be more based on conf. Could be limited by system (ulimit -n) #
worker_rlimit_nofile 1024;

events {
    # Default 1024 #
    worker_connections  256;
}

# Error Log #
error_log  logs/error.log;
error_log  logs/error.log  notice;
error_log  logs/error.log  info;
# Process ID Log #
pid        logs/nginx.pid;

http {
    include       mime.types;
    default_type  application/octet-stream;

    # Access Logs #
    map $request_uri $loggable {
        default                                             1;
        ~*\.(ico|css|js|gif|jpg|jpeg|png|svg|woff|ttf|eot)$ 0;
    }
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$request_body_file"';
    access_log  logs/access.log  main buffer=32k flush=30m if=$loggable;

    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    #gzip  on;
        
    # Max user upload size #
    client_max_body_size 20M;
    # Uploaded file RAM buffer instead of temp file #
    client_body_buffer_size 20M;
    # store request body in temp file for debugging #
    # client_body_in_file_only on;


    # Root domain HTTP #
    server {
        listen 80;
        server_name mydomain.com;
        return 301 https://$server_name$request_uri;
    }
    server {
        listen 80;
        server_name app1.mydomain.com;
        return 301 https://$server_name$request_uri;
    }
    server {
        listen 80;
        server_name app2.mydomain.com;
        return 301 https://$server_name$request_uri;
    }
    # Root domain HTTPS #
    server {
        listen 443 ssl;
        ssl_certificate      /etc/letsencrypt/live/mydomain.com/fullchain.pem;
        ssl_certificate_key  /etc/letsencrypt/live/mydomain.com/privkey.pem;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        location / {
            return 403;
        }
    }

    # App 1 #    
    server {
        listen 443 ssl;
        server_name app1.mydomain.com;
        ssl_certificate /etc/letsencrypt/live/app1.mydomain.com/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/app1.mydomain.com/privkey.pem; # managed by Certbot

        location / {
            proxy_pass         http://localhost:3000;
        }
    }
    
    # App 2 #    
    server {
         listen 443 ssl;
         server_name app2.mydomain.com;
         ssl_certificate /etc/letsencrypt/live/app2.mydomain.com/fullchain.pem; # managed by Certbot
         ssl_certificate_key /etc/letsencrypt/live/app2.mydomain.com/privkey.pem; # managed by Certbot

         location / {
            proxy_pass         http://localhost:5000;
         }
    }
}
  • A cleaner solution would have regex based HTTP redirects to HTTPS subdomains instead of multiple HTTP server blocks and wildcard SSL certificates instead of different SSL certificates for each subdomain.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment