February 6, 2021

Another Nginx SNI Example with Ghost & BeamCafe & Pinry Deployment

Another Nginx SNI Example with Ghost & BeamCafe & Pinry Deployment

1. Intro

Ghost is a free and open source blogging platform written in JavaScript and distributed under the MIT License, designed to simplify the process of online publishing for individual bloggers as well as online publications.
BeamCafe knows very little about you - the names of your files, your IP and well, that's it. If you send over a file it won't get saved somewhere on the server but instead will be streamed from your local machine over the server of beam.cafe directly to your peer. You can always check who's downloading or streaming your files and, in case a link fell into the wrong hands, invalidate a file.
Pinry, a tiling image board system for people who want to save, tag, and share images, videos and webpages in an easy to skim through format.

2. Background

This is an example of the use of Nginx SNI to deploy three applications on the same port 443.
Environment: Ubuntu 20.04 LTS
Ghost & Pinry deployment via Docker
Beam.Cafe deployment via bare VPS setup

3. Dependencies

Install the required packages

apt -y update
apt -y install build-essential nginx python3-certbot-nginx git curl
curl -sL https://deb.nodesource.com/setup_15.x | sudo -E bash -
apt -y install nodejs 
npm install -g pm2

Install Docker

curl -sSL https://get.docker.com/ | sh
systemctl enable --now nginx docker

Install docker-compose (check latest version yourself)

curl -L https://github.com/docker/compose/releases/download/1.27.4/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

4. Ghost Deployment

Create directory and edit docker-compose file

mkdir -p /opt/ghost && cd /opt/ghost && nano docker-compose.yml

Fill in the followings:

version: '3.7'

services:

  ghost:
    image: ghost:3-alpine
    restart: always
    ports:
      - 127.0.0.1:3000:2368
    environment:
      database__client: mysql
      database__connection__host: db
      database__connection__user: root
      database__connection__password: password #edit
      database__connection__database: ghost
      url: https://ghost.example.com #your domain
      mail__transport: SMTP
      mail__from: user <[email protected]> # optional, your email of own domain
      mail__options__service: SMTP
      mail__options__host: smtp.youremailserver.com # optional, eg. gmail
      mail__options__port: 587
      mail__options__auth__user: [email protected] # optional, eg. gmail
      mail__options__auth__pass: your_auth_pass # optional, eg. gmail
    volumes:
      - /opt/ghost/content:/var/lib/ghost/content

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: password # edit
    volumes:
      - /opt/ghost/mysql:/var/lib/mysql

Build up

docker-compose up -d

5. BeamCafe Deployment

Pull front-end and back-end project files

cd /opt 
git clone https://github.com/dot-cafe/beam.cafe.git 
git clone https://github.com/dot-cafe/beam.cafe.backend.git

To build the front end, first enter the project directory

cd /opt/beam.cafe

Then edit this file

nano webpack.config.prod.js

Replace the domain name of WS_ENDPOINT and HTTPS_ENDPOINT with your own:

   plugins: [
        new webpack.DefinePlugin({
            'env': {
                'NODE_ENV': JSON.stringify('production'),
                'VERSION': JSON.stringify(resolveAppVersion()),
                'BUILD_DATE': JSON.stringify(Date.now()),
                'WS_ENDPOINT': JSON.stringify('wss://beamcafe.example.com/ws'),
                'HTTPS_ENDPOINT': JSON.stringify('https://beamcafe.example.com')
            }
        }),

Then install the dependencies and you can build

npm install
npm run build

Move the built front-end resource files to the Nginx site directory

mv dist/ /var/www/beam

Next, build the backend

cd /opt/beam.cafe.backend
npm install
npm run build

After the backend is built, use pm2 to start the backend and set it to start automatically

pm2 start dist/src/app.js --name beam.cafe.backend
pm2 startup
pm2 save

6. Pinry Deployment

Create a new directory and docker-compose file

mkdir -p /opt/pinry && cd /opt/pinry && nano docker-compose.yml

Fill in the followings:

version: "3.7"

services:
  pinry:
    image: getpinry/pinry
    container_name: pinry
    restart: always
    ports:
      - 9090:80
    volumes:
      - /opt/pinry/data:/data

Build up

docker-compose up -d

Next, use the following command to create an administrator account

docker-compose exec pinry python manage.py createsuperuser --settings=pinry.settings.docker

If necessary, you should now edit the Pinry configuration file

nano /opt/pinry/data/local_settings.py

Some functions of Pinry can be set here:

ALLOW_NEW_REGISTRATIONS = False # close registration
PUBLIC = False # show content only after login

After modifying the Pinry configuration, to make it effective, the Pinry container must be restarted

docker-compose restart pinry

7. Nginx Configuration

7.1 Nginx main config file

nano /etc/nginx/nginx.conf

Add the following configuration outside the “http block”

stream {
        map $ssl_preread_server_name $example_multi {
                ghost.example.com ghost;
                beamcafe.example.com beamcafe;
                pinry.example.com pinry;
        }
        upstream ghost {
                server 127.0.0.1:20001;
        }
        upstream beamcafe {
                server 127.0.0.1:20002;
        }
        upstream pinry {
                server 127.0.0.1:20003;
        }
        server {
                listen 443      reuseport;
                listen [::]:443 reuseport;
                proxy_pass      $example_multi;
                ssl_preread     on;
        }
}

7.2 HTTP Redirect

nano /etc/nginx/conf.d/httpredirect.conf
#HTTP Redirect
server {
        listen 80;

        server_name ghost.example.com;
        if ($host = ghost.example.com) {
                return 301 https://$host$request_uri;
        }

        server_name beamcafe.example.com;
        if ($host = beamcafe.example.com) {
                return 301 https://$host$request_uri;
        }

        server_name pinry.example.com;
        if ($host = pinry.example.com) {
                return 301 https://$host$request_uri;
        }

        return 404;
}

7.3 Ghost config file

nano /etc/nginx/sites-available/ghost.example.com
server {
  listen 127.0.0.1:20001 ssl; # match port in nginx.conf

  ssl_certificate       /your/path/to/cert/example_cert.pem;
  ssl_certificate_key   /your/path/to/cert/example_key.pem;
  ssl_session_timeout 1d;
  ssl_session_cache shared:MozSSL:10m;
  ssl_session_tickets off;

  ssl_protocols         TLSv1.2 TLSv1.3;
  ssl_ciphers           ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers off;

  server_name           ghost.example.com; # your domain

  location / {
    proxy_redirect off;
    proxy_pass http://127.0.0.1:3000; # match port set in your Ghost docker-compose
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

7.4 BeamCafe config file

nano /etc/nginx/sites-available/beamcafe.example.com
server {
   listen               127.0.0.1:20002 ssl; # match port in nginx.conf
   server_name          beamcafe.example.com; # your domain
   client_max_body_size 0;
   root                 /var/www/beam;
   index                index.html;

  ssl_certificate       /your/path/to/cert/example_cert.pem;
  ssl_certificate_key   /your/path/to/cert/example_key.pem;
  ssl_session_timeout 1d;
  ssl_session_cache shared:MozSSL:10m;
  ssl_session_tickets off;

#  client_max_body_size 0;

  ssl_protocols         TLSv1.2 TLSv1.3;
  ssl_ciphers           ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers off;


location ~ ^/(d|b) {
    proxy_pass                  http://127.0.0.1:8080$request_uri;
    proxy_buffering             off;
    proxy_request_buffering     off;
}

location /ws {
    proxy_pass                  http://127.0.0.1:8080;
    proxy_buffering             off;
    proxy_set_header Host       $http_host;
    proxy_set_header X-Real-IP  $remote_addr;
    proxy_http_version          1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout          86400;
}

}

7.5 Pinry config file

nano /etc/nginx/sites-available/pinry.example.com
server {
  listen 127.0.0.1:20003 ssl;

  ssl_certificate       /your/path/to/cert/example_cert.pem;
  ssl_certificate_key   /your/path/to/cert/example_key.pem;
  ssl_session_timeout 1d;
  ssl_session_cache shared:MozSSL:10m;
  ssl_session_tickets off;

  ssl_protocols         TLSv1.2 TLSv1.3;
  ssl_ciphers           ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers off;

  server_name           pinry.example.com;
    add_header Content-Security-Policy upgrade-insecure-requests; 

  location / {
    proxy_redirect off;
    proxy_pass http://127.0.0.1:9090;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}
ln -s /etc/nginx/sites-available/ghost.example.com /etc/nginx/sites-enabled/ghost.example.com
ln -s /etc/nginx/sites-available/beamcafe.example.com /etc/nginx/sites-enabled/ghost.example.com
ln -s /etc/nginx/sites-available/pinry.example.com /etc/nginx/sites-enabled/ghost.example.com

7.7 Get SSL

You can get SSL cert via CertBot

certbot --nginx

Or, set path directly if you already have the wildcard certificates.
Use CloudFlare Certificate with Nginx + V2RAY + WebSocket +TLS + CDN

7.8 Ready to start

First, make sure the ports 3000(Ghost), 8080(BeamCafe), 9090(Pinry) are all being listened.

netstat -nltp

Check Nginx

nginx -t

Restart Nginx

systemctl restart nginx

8. All set

Do not forget to set DNS record of your subdomains.
If you follow the steps above, now you can visit

Enjoy.