Firefox Send on Shared Hosting

      No Comments on Firefox Send on Shared Hosting

To setup Firefox Send on shared hosting you will need a Shared hosting that supports NodeJS.

SSH support is very much recommended but not necessary for non-production service.

Following the following steps:

1. Prepare dist/

You probably shall have issue running npm run build since you can't npm link webpack which requires sudo which is impossible under shared hosting environment.

On you local machine, grab /app/dist/ in the official Docker image: docker run

then do a docker cp <container-id>:/app/dist ./ .

Zip the directory: tar zcvf dist.tar.gz dist.

2. Prepare hosting

SSH into the instance or use the Web SSH: if your host does not have SSH support, continue but your instance won't be able to handle production workload.

Clone the latest code to public_html(for DirectAdmin: adjust as needed) git clone , or upload with zip file and unzip.

Setup hosting as usual NodeJS website, probably with NodeJS Selector. Use NodeJS >=16. Setup SSL certs, etc.Use production if you have SSH access and can setup Redis; If not use development with caveats listed at .

Setup environment variables per .

Entrypoint is server/bin/prod.js.

3. (Optional but very much recommended) Setup Redis

If you have SSH access, follow but tell Redis to listen on 6379:

# create a unix domain socket to listen on
unixsocket /tmp/redis.sock
# set permissions for the socket
unixsocketperm 775
# No password
# requirepass passwordtouse
# Do not listen on IP
port 6379
daemonize yes
stop-writes-on-bgsave-error no
rdbcompression yes
# maximum memory allowed for redis - 50MB for small site, 128MB+ for high traffic
maxmemory 50M
# how redis will evice old objects - least recently used
maxmemory-policy allkeys-lru

If you don't have Redis it may be possible to transport Redis binary to the server to bypass building step but I have not tried that yet.

4. Fake install


4.1 With SSH

Follow command proposed by NodeJS Selector to enter Virtual Environment.

Run npm install.

4.2 Without SSH

Use NodeJS Selector to conduct npm install.

5. Combine dist/ and code

Upload the dist.tar.gz created in Step 1 to the same folder of the code. Unzip.

IMPORTANT: server and dist should be under the same folder after this step.

6. Start Server

Start server and everything should be working.

Potential Issues

UI not working. Tons of underfined in requests

You did not run npm build. Can you setup webpack? If so setup and build; If not follow Step 5.

403 Error

Did you start the server?

500 Error

  • Did you setup Redis? If not see Step 1.
  • Is everything accessible? Double check environment variables.
  • Failed npm build: See Step 5 to do a combination to bypass this step.

macOS: Convert multi page PDF to multiple images by command


  • shall ONLY WORK with ONE PAGE.
  • Most commands on Stackoverflow shall ask you to use imagemagick : however recommended commands are for Linux which works different from macOS which is BSD-based.
  • Use this tried approach:


In terminal:

Install Homebrew

/bin/bash -c "$(curl -fsSL"

Install imagemagick with Homebrew

brew install imagemagick

Add the following code to your ~/.bash_profile or ~/.zshrc depends on which shell you are using

function pdf2jpg () { convert +adjoin -verbose -background white -alpha remove -alpha off -density 300  $1 -quality 100 -sharpen 0x1.0 $1-%04d.jpg }
function pdf2jpgpw () { convert -authenticate $2 +adjoin -verbose -background white -alpha remove -alpha off -density 300  $1 -quality 100 -sharpen 0x1.0 $1-%04d.jpg }

Reload your shell or source ~/.bash_profile / source ~/.zshrc.

These commands can be called like

pdf2jpg xxx.pdf

which will generate a bunch of JPG files under the same folder; or, if the PDF is password protected,

pdf2jpg xxx.pdf password

will have the same effect.

Use DirectAdmin/cPanel to run ANY WEB APPLICATION in ANY LANGUAGE!


Echoing my comments in -

Since Apache can be used as reverse proxy why should we be limited by environment provided by the host?


Apache ProxyPass (nope)

Of course Apache can act as reverse proxy natively - but this function is almost guaranteed to be disabled in shared hosting environment.

CloudLinux + Python Selector + Passenger (not natively)

DirectAdmin, or cPanel, is extremely flexible since it's a wrapper on top of Apache(aka LiteSpeed). Most hosts are using CloudLinux, which usually comes with Python Selector, although the logic behind is to ensure full isolation between users and allocate resources among every user for anti abuse.

Python(or Ruby/NodeJS) Selector is developed on Passenger, a plugin for Apache: starting from version 6.0 it's possible to use ANY language with this plugin as long as the app is taking in a port argument. However this does not seems possible with Apache, leaving us looking for other solutions.

CloudLinux + Python Selector + Passenger + Python WSGIProxy + flock

We can design a system like this:

  • DirectAdmin use CloudLinux as host
  • CloudLinux has Passenger installed exposing Python Selector
  • Write a custom WSGI Proxy talking to the app locally
  • Make the app listen to localhost and a port
  • Ensure the app is always running


You want SSH access to the host.

Upload application to host

Upload the application you want to run to the host. Make it run by listening to a random local port and Use curl to ensure the app is actually running.

Setup Python Selector

Use any Python 3 provided.

Setup files

Enter the virtualenv by copy and pasting the command provided. looks like:

import imp
import os
import sys

sys.path.insert(0, os.path.dirname(__file__))

wsgi = imp.load_source('wsgi', '')
application = wsgi.application

Create an with:

import sys, os

import webob     #
import wsgiproxy #

BACKEND_HOST = os.getenv("ROCKET_ADDRESS", "localhost")
BACKEND_PORT = os.getenv("ROCKET_PORT", 30000)

PROXY = wsgiproxy.HostProxy(BACKEND_URL)

def application(environ, start_response):
    req = webob.Request(environ)

    res = req.get_response(PROXY)

    start_response(res.status, res.headerlist)
    return [res.body]

Create a requirements.txt with:


Change the environment variables with:

  • ROCKET_PORT: the port your application shall listen on

Save the config. Use panel to install dependencies, or use SSH to run pip install -r requirements.txt.

Test your site: it should be working.

Setup application monitoring

In the "Cron Job" section in the panel, setup

flock -nx ~/tmp/app.lock -c "THE_STARTUP_COMMAND_OF_YOUR_APP"

Disable Email, and set the cron to * * * * * to check every minute.


Use Mailgun/SendGrid for free inbound SPAM filtering


Although not problematic for most commercial Email providers(GMail, Outlook, etc.), self-hosted Email are prone to inbound SPAM.

A couple solutions exist: the most common one is to use local Spamassassin - but training the model takes time and there's no good corpse available - and spammers are much smarter.

There are a handful of hosted Inbound SPAM filtering solutions but are quite expensive:

  • Mailchannels - $20/month/5 domains, then hikes to $507/mth/1000 domains.
  • Spamtitan - pricing is "quotation only".
  • Spydermail - $1.99/mailbox/month, starting from 15 mailboxes.
  • MXguarddog - $0.25/mailbox/month.
  • Spamhero - per mailbox per mail pricing.
  • McAfee: pricing not available.

Use Mailgun

Mailgun is included in Github student package. Flex plan provides 1000 Emails/month for free.

Code of connector:

import os

from flask import Flask, request, jsonify
from imap_tools import MailBox

app = Flask(__name__)

# HTTP param > environment variable > default

IMAP_SERVER = os.getenv('IMAP_SERVER', '')


def get_target_mailbox(data, use_bool=MAILGUN_ANTISPAM_USE_BOOL, sscore_cutoff=MAILGUN_ANTISPAM_SSCORE_CUTOFF):
    Return target mailbox from the data received from Mailgun.

    for more details.


    :param junk_threshold: threshold for junk mail according to Mailgun
    :type junk_threshold: float
    :param data: mail data from Mailgun
    :type data: dict
    :return: INBOX/Junk
    :rtype: str
    # No = Not spam, Yes = Spam
    # "At the time of writing this, we are filtering spam at a score of around 5.0 but we are constantly calibrating
    # this."
    if use_bool:
        if data['X-Mailgun-Sflag'][0] == 'No':
            return 'INBOX'
        return 'Junk'

    # lower = less likely to be spam
    # negative = very unlikely to be spam
    # > 20 is very likely to be spam
    if float(data['X-Mailgun-Sscore'][0]) < sscore_cutoff:
        return 'INBOX'
    return 'Junk'

@app.route('/post_mime', methods=['POST'])
def post_mime():
    # TODO: check if the request is from Mailgun
    data = request.form.to_dict(flat=False)

    # HTTP param suppresses everything else
    imap_username = request.args.get('username', IMAP_USERNAME)
    imap_password = request.args.get('password', IMAP_PASSWORD)
    imap_server = request.args.get('server', IMAP_SERVER)
    use_bool = int(request.args.get('use_bool', MAILGUN_ANTISPAM_USE_BOOL))
    sscore_cutoff = int(request.args.get('sscore_cutoff', MAILGUN_ANTISPAM_SSCORE_CUTOFF))

    # decide whether incoming mail is spam or not
        target_mailbox = get_target_mailbox(data, use_bool, sscore_cutoff)
    except Exception as e:
        return jsonify({'error': 'Cannot decide target: ' + str(e)}), 500

    # directly deliver to target mailbox
        with MailBox(imap_server).login(imap_username, imap_password) as mailbox:
            msg = '\n'.join(data['body-mime'])
            mailbox.append(msg.encode(), target_mailbox, dt=None)
    except Exception as e:
        return jsonify({'error': str(e), 'status': -1}), 500

    return jsonify({'status': 0})

if __name__ == '__main__':'localhost', port=8000, debug=True)

Setup environment variables as follows:

Name    Value   

IMAP_PASSWORD   your password

IMAP_SERVER the server  

IMAP_USERNAME   login   

LANG    en_US.UTF-8 

LC_ALL  en_US.UTF-8 





Go to Mailgun, setup a Route with catch all forwarding to https://your-binded-domain/post_mime .

Nextcloud + Collabora CODE+ Docker Compose + Nginx


Google Docs is great - but is behind GFW. Better have something we can control.


Nextcloud has native support of Collabora Office.

ONLYOFFICE also supports Nextcloud.


Collabora is LibreOffice in browser. Harder on server since the server is effectively rendering a copy of LibreOffice for every client and transporting differences after operations. Works better if bandwidth(for both server and client) is high, and server is powerful.

ONLYOFFICE is more like Google Docs: client's browser loads the full editor, and sync changes on the document to the server. Easier on server but mobile client is not free.

Setting up Collabora

Get Nginx working

I use Nginx on the host directly since I have more than 1 services to run.

Setup SSL with anything you like - I use

server {
    listen 80;
    listen [::]:80;
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name .com;

    ssl_certificate /root/;
    ssl_certificate_key /root/;
    ssl_session_timeout 5m;
    ssl_protocols  TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    keepalive_timeout 70;
    ssl_session_cache shared:SSL:10m;
    ssl_dhparam /etc/nginx/ssl/dhparams.pem;

    # static files
    location ^~ /browser {
       proxy_set_header Host $http_host;

    # WOPI discovery URL
    location ^~ /hosting/discovery {
      proxy_set_header Host $http_host;

    # Capabilities
    location ^~ /hosting/capabilities {
      proxy_set_header Host $http_host;

    # main websocket
    location ~ ^/cool/(.*)/ws$ {
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "Upgrade";
      proxy_set_header Host $http_host;
      proxy_read_timeout 36000s;

    # download, presentation and image upload
    location ~ ^/(c|l)ool {
      proxy_set_header Host $http_host;

    # Admin Console websocket
    location ^~ /cool/adminws {
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "Upgrade";
      proxy_set_header Host $http_host;
      proxy_read_timeout 36000s;

Setup Collabora CODE server

I really hate this setup since privileged is required - there's no easy way bypassing the limit:

version: '3'
    image: collabora/code:latest
    restart: always
    privileged: true
      - password=xxx
      - username=xxx
      - domain=nextcloud.your.domain
      - ''

So all traffic shall go through the reverse proxy.


Install the App.


Redis Object Cache on Shared Hosting (DirectAdmin, cPanel, Plesk, etc.) without native support

What Cache?

There are may layers of cache one can run on WordPress - ranking from farthest to closest:

  • Browser: setup long expiry time to cache as much data as possible on user's machine.
  • CDN: For static content like CSS, image, JS and rich media. Works wonder if the whole page can be cached altogether. Jetpack has free CDN for image and CSS without purging possibillity, while Cloudflare can be more flexible but your mileage may vary. Distributed.
  • Cache on Load Balancer: Some LBs like LiteSpeed and Nginx can have cache enabled. Normally not distributed.
  • "Supercache": Caching the whole page to disk, DB or RAM to turn the dynamic site into static one. Works great for CMS, but won't work that much for sites with tons of interactions, like WooCommerce. Most famous plugins works on this level, like WP Super Cache, or the LiteSpeed plugin with crawler. Can be distributed.
  • Object Cache: Cache the raw DB query to ease up DB load, serialize object and save in RAM, like the Redis cache covered in this article. Can be distributed but this article only covers the most basic single instance config.
  • OPCode cache: PHP can cache some calculations in RAM to save on CPU. Configured in php.ini. Not distributed.

Why go this far?

This task is trivial if you control the whole environment, however there are tons of restrictions on shared hosting. But if you have SSH access there are things you can take advantage of.


If your machine has gcc

Download latest Redis

SSH into your shared hosting box.

Download and unzip the latest version of Redis 6:

cd ~
tar zxvf redis-stable.tar.gz
mv redis-stable redis
rm redis-stable.tar.gz
cd ~/redis

If you want easier setup, download Redis 5 at

Compile the server

make redis-server

Your redis-server should be available at ~/redis/src/redis-server.

If your machine does not have gcc

Find our your OS

Type uname -a. This will give you your kernel version, but might not mention the distribution your running.

To find out what distribution of linux your running (Ex. Ubuntu) try lsb_release -a or cat /etc/release or cat /etc/issue or cat /proc/version.

Compile Redis on that distro

Get a VM/container of that distro, follow similar steps and take redis-server out.

Try at the target machine and see if everything's working. Redis is pretty easy to build.

Create Redis config file

Create a redis config file somewhere, say ~/redis/rediss.conf:

You can't use /tmp as you are on shared hosting. Create a temporary folder - this folder exists on DirectAdmin by default:

mkdir ~/tmp
cd ~/tmp

and keep the absolute path of your tmp folder.

Content of file for Redis 6:

bind 0
protected-mode yes
port 0
unixsocket {TMP_FOLDER}/redis.sock
unixsocketperm 700
timeout 0
tcp-keepalive 300
daemonize yes
pidfile {TMP_FOLDER}/
maxmemory 50M

And for Redis 5:

# create a unix domain socket to listen on
unixsocket {TMP_FOLDER}/redis.sock
# set permissions for the socket
unixsocketperm 775
# No password
# requirepass passwordtouse
# Do not listen on IP
port 0
daemonize yes
stop-writes-on-bgsave-error no
rdbcompression yes
# maximum memory allowed for redis - 50MB for small site, 128MB+ for high traffic
maxmemory 50M
# how redis will evice old objects - least recently used
maxmemory-policy allkeys-lru

Save it.

Start the process

Most distros should come with flock to ensure that only one instance of process can be run at the same time. If not, refer to and make your own flock.


flock -nx ~/tmp/redis.lock -c "~/redis/src/redis-server ~/redis/rediss.conf"

in the shell.

ls ~/tmp and see whether the lock file and the socket file are generated. If everything's there, you should have a Redis instance running.

Configure WordPress

In wp-config.php:

/** Redis object cache */
define( 'WP_REDIS_SCHEME', 'unix' );
define( 'WP_REDIS_PATH', '{TMP_FOLDER}/redis.sock' );
// define( 'WP_REDIS_PASSWORD', 'secret' );
define( 'WP_REDIS_TIMEOUT', 1 );
define( 'WP_REDIS_READ_TIMEOUT', 1 );

// change the database for each site to avoid cache collisions
define( 'WP_REDIS_DATABASE', 0 );
define( 'WP_REDIS_MAXTTL', 60 * 60 * 24 * 7 );
// define( 'WP_REDIS_DISABLED', true );

Save it.

Enable Redis Object Storage

Go to Plugins, and install Redis Object Storage. Enable it. See whether it can talk to your local Redis.

Setup cronjob to ensure Redis is always up

Although we may not care contents inside Redis it's important to make sure it's always up.

In your shared hosting's control panel, add a cronjob for every minute, with content

flock -nx ~/tmp/redis.lock -c "~/redis/src/redis-server ~/redis/rediss.conf" >/dev/null 2>&1

to restart process and don't send Email.

Now you should enjoy the new layer of caching.

Replacement for free G Suite without losing GMail with Gmail, SMTP and forwarder

What happened?

Sadly Google is shutting down the free version of G Suite for good.

What now?

There are some not-so-good alternatives:

  • Self hosting: Mailcow is extremely memory hungry and delivery is definitely spotty unless you use a external SMTP provider like Mailgun.
  • Yandex: Free, 10GB space, 1000 seats. Anti SPAM is very hungry and sending can be limited for no good reason.
  • VK Mail( Free, unlimited space, 5000 seats. Delivery is spotty.
  • MXRoute: Good service, reliable. Price can be as low as $10/yr/10GB during black friday. 1GB mailbox can be as low as $3/year, 2GB at $5/year. There's a $179/lifetime/10GB package. Check Nexusbytes - price is better with resellers.
  • MS Exchange: $4/seat.
  • Office 365: $99/yr/6 seats.
  • Paying ransom to Google: $72/year.
  • Apple: $1/seat/month. No unlimited alias.
  • Zoho: Free, 10GB space but NO SMTP/IMAP support.
  • Disroot: Paid at 0.15 Euro/GB/yr. Delivery can be problematic.
  • Protonmail: $5/sear/month. NO SMTP support.


So what I am looking should have:

  • Large enough space(I've accumulated ~8GB of Emails in the last 15ish years)
  • Good delivery
  • SMTP support
  • Unlimited aliases
  • Unlimited seats
  • Catch-all address

Gmail has a unique feature(not that unique but I've only seen it on Outlook outside Gmail) - "send as alias". You can setup a preferred alias, customize SMTP credentials and Gmail shall call that SMTP server when sending outbound Email when using that alias.
So what we can do is:

  • Get Email forwarding service on the domain to catch Emails and forward to your personal Gmail
  • Get SMTP working
  • Add SMTP as alias in Gmail


You will need

  • A Email forwarder(will elaborate)
  • Preferred SMTP provider
  • Gmail account(can be free ones)
  • DNS access of your domain

Email forwarder

A Email forwarder should act as your MX record, take inbounding Email and forward to designated address.
There are quite a number of good free forwarders around:

SMTP provider

There's no lack of them with free plans:

  • Mailchimp
  • Sendgrid
  • Sparkpost
  • Amazon SES(note traffic is not free)


Make sure you stick with this sequence.

(Optional) Migrate existing Emails

You can do that in Settings.

Setup Email forwarding

Go to any forwarder and setup MX and SPF record with DNS provider: MX record to receive Email, SPF to send Email on behalf of your domain so you can receive forwarded Email.

Setup SMTP

Use any SMTP provider and verify your domain. Add necessary DMARC and SPF record: the SPF record may looks like v=spf1 ~all
Note down the SMTP credentials. Using Sparkpost as example -

Port 587
Alternative Port 2525
Authentication AUTH LOGIN
Encryption STARTTLS
Username SMTP_Injection
Password an API key you created

Setup Gmail

  1. Go to Gmail's settings and select "send as alias". Put in your desired alias.

  2. Click next: Gmail may infer some IMAP settings - replace with SMTP credentials.

  3. Click next: Gmail will send an Email to that alias for verification - you should receive that Email in your Gmail as you've already setup forwarding. Put in the code.

  4. (Optional) Set the new alias as default.

Now you've got a reliable free Domain Email for completely free.

Hacking Outline Wiki: Making Slack login works with other methods, like OIDC

There's a problem that Slack login is required for Slack integration, but sometimes we want to use OIDC for login: this will make the application vomit.
Steps to fix it:

  1. Get a Postgres connector, connect to DB. Backup DB.

  2. Consider the Email domain you will use with OIDC: blah [at] is, is

  3. Fill in all environment variables required for OIDC integration. Remove ALLOWED_DOMAIN variable - otherwise Outline will not allow logins from Email whose domain is different from it's own domain.

  4. Create a new entry in table authentication_providers. name is always oidc, domain is the domain of your Email, enabled is true, teamid is the same as the original one. Note self hosted Outline can only have 1 team at a time.

  5. Go to table teams. The name is ALWAYS HARDCODED as Wiki. domain is empty. Failing to do so will cause Outline to complain "max number of teams reached".

  6. Now your new OIDC should be working but users is not be associated across authentication providers by Email.

  7. In user_authentications table, create a second entry for users you want to associate: random id, same userId, set authenticationProviderId, scopes and providerId as the new one's. Delete the newly created but disassociated user.

  8. Now you should have more than 1 login method with users associated across the board by Email.

Free personal wiki with Outline on Heroku with personal domain via Cloudflare

Outline what?

Outline is a nice personal wiki tool that

  • Supports Markdown natively
  • Self hosting option
  • Fast
  • Nice searching function
  • Not overly complicated
  • Supports live collaboration

making it great for hosting personal wiki.
However there's no free official hosted version of Outline - but it's pretty simple to host on Heroku for free.



Register an account, and bind a credit card: this will give you 1,000 hours of running time per month, which is good enough. A worker dyno is needed so this shall give you 500 hours of usable time per month.


Slack app is required for authentication and integration.
Go to and create an app - in a space you feel comfortable.

Other authentication

OIDC is supported but I have not tried it - Auth0 and Okta have free plan that supports OAuth.


S3 is needed for rich media storage.
There are a couple of free providers:

  • Backblaze B2: 10GB space, 1GB download traffic per day. Technically should work with Cloudflare but I have not figured out how.
  • Scaleway: 75GB space, 75GB/month traffic. DC located in EU.
  • IBM: 25GB space, 5GB/month traffic.
  • Storj DCS: 150GB space, 150GB traffic.

Create an account and collect all the details.


Get something that supports SMTP. Free options includes:

Create an account, verify the domain and collect SMTP config.

(optional) Cloudflare

Use Cloudflare to use your own domain. A free plan is good enough but you may need a Page Rule for overriding SSL settings.

(optional) GitHub account

Use this to update the application in the future, or configure auto update.
Fork the repo;
Install wei/pull, enable it for your forked repo so your personal fork is always up to date.


Instructions as follow:

Get it running first

Click here to deploy the application. Follow the instructions, and put in environment variables as requested. Generate UTILS_SECRET with openssl rand -hex 32. URL is the for now.
Do not bother with Google OAuth - you need a paid account to get it working.
You should have a working app: try logging in. If in doubt, check logs under More.
Remember to enable workers at Configure Dynos under Overview.

Cloudflare setup

This official documentation is not very clear - TLDR version:

  • Add your desired domain to Heroku, don't bother with SSL - it's paid addon
  • Go to Cloudflare, add a CNAME record from that domain to - NOT the record Heroku gave you!
  • If your SSL settings is NOT Strict, add a Page Rule to override it;
  • Change URL in Heroku config to the new domain under Settings.

Now you should have access on the new domain. It may take a while for Cloudflare to issue the new SSL certificate.

Setting up app update

Go to Deploy, connect your Github, select your fork.
Enable auto update as you wish(de facto nightly build), or conduct manual updates.

Database backup(for Heroku Postgres)

It seems that there's no way to set this up in the UI:

  • Go to and click in the Postgres DB
  • Settings - View credentials
  • Copy the Heroku CLI command, something like heroku pg:psql postgresql-blah-12345 --app outline-yourname
  • In your local shell that has heroku setup, run heroku pg:backups:schedule postgresql-blah-12345 --at '02:00 America/Los_Angeles' --app outline-yourname, change the time as your preference.

Limitation for free DB:

  • Only 1 backup per day
  • No point in time
  • Only preserving the last 2 backups