Skip to content

Instantly share code, notes, and snippets.

@osamaqarem
Last active March 28, 2024 18:37
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • 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.

# nginx.conf

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:

# nginx.conf

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/mydomain.com/ and the symbolic ones at /etc/letsencrypt/live/mydomain.com/.

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;
         }
    }
}

Suggestions

  • Regex based HTTP redirects to HTTPS subdomains instead of multiple HTTP server blocks.

  • Wildcard SSL certificate instead of different SSL certificates for each subdomain.

  • The SSL certificate generated here achieves a rating of B on SSL labs. Todo for A+?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment