Installing Bicrypto

How to install Bicrypto on a Linux server — the guided installer, the manual path, licence activation, and how to prove the install actually works.

15 min readUpdated 3 August 2026

An install is not one program. It is a Node application supervised by PM2, a MySQL database, a Redis server, and a reverse proxy terminating TLS in front of both. You need a Linux box with root SSH access. Shared hosting cannot run this — the platform needs to bind ports, install system packages and keep long-lived processes alive.

Budget 20–40 minutes of wall time. The installer prints "10-15 minutes"; the frontend build is the long pole and on a small VPS it alone can take longer than that.

What ends up running

pnpm start hands three applications to PM2, defined in production.config.js:

App What it is Port
backend The REST API and every WebSocket endpoint (backend/dist/index.js) NEXT_PUBLIC_BACKEND_PORT, default 4000
frontend The Next.js site (next start) 3000, hardcoded
cron The scheduler — same code, CRON_MODE=only 4001, serves no traffic

The frontend port is set inside the PM2 env block, so NEXT_PUBLIC_FRONTEND_PORT in .env does not move it. Port 4001 exists purely so the scheduler does not collide with the API on 4000; never point a load balancer or proxy at it.

Alongside those you need MySQL (or MariaDB) and Redis. Redis is not a cache you can skip: sessions, rate limits, distributed locks, the BullMQ scheduler and cross-process settings invalidation all live in it, and the backend exits with code 78 at boot if it cannot reach it.

Before you start

  • Ubuntu 24.04 LTS or newer, with root SSH. 24.04 is the floor: the backend's HTTP server is a prebuilt binary linked against glibc 2.38, which Ubuntu 22.04 does not have. The installer reads /etc/os-release and picks apt, dnf or yum in that order, but it never checks the release — on 22.04 it installs happily and the backend then crash-loops. See Server requirements.
  • Node.js 22, 24 or 26 — nothing else. The guided installer installs Node 26.
  • A reachable MySQL or MariaDB server with a user that can create tables.
  • 4 GB RAM and 10 GB free disk. Below either, the installer warns and asks whether to continue.
  • A domain pointing at the box, and a TLS certificate. See HTTPS is not optional.
  • Your Envato purchase code. Without activation the platform serves 403 on nearly every API route.

Nothing in installer.sh installs mysql-server or mariadb-server. It only prompts for connection details and connects. Install and start your database first, or the install fails at the schema import with the platform half configured.

Getting the files

Download the release archive from your Envato downloads for item 35599184 and unpack it into the directory that will be the application root — for example /var/www/bicrypto. Everything runs from there; there is no separate public directory to point a vhost at.

Two files must sit at the top of that directory:

  • installer.sh — the guided installer.
  • initial.sql — the database schema. The installer exits with an error if it is not in the working directory, and pnpm seed has nothing to seed into without it.

Later releases are applied in place from Admin → System → Updates, which downloads and extracts over the same tree. That screen requires an activated licence, so a first install always starts from the archive.

The guided installer

Run this from the application root, as root.

  1. Change into the application directory. The installer uses the working directory as the application root — it never asks where the code is.

    cd /var/www/bicrypto
  2. Run it with no arguments. Anything else is a repair mode, not an install.

    bash installer.sh

    Everything it prints is also appended to /var/log/bicrypto-installer.log, including the parts that scroll past. When something goes wrong, that file is the record, not your terminal scrollback.

  3. Confirm, then answer the prompts. It asks Continue with installation? [Y/n] and then works through twelve steps, stopping for input at the ones listed under What it asks you. Nothing is destructive until the schema import, which asks first if the database already has tables.

  4. Leave it alone during the build. Steps 8 of 12 (pnpm install and pnpm build:frontend) are where the time goes. A silent death here is almost always the kernel OOM killer on a small box — the frontend build is launched with a 7780 MB heap ceiling.

  5. Read the summary. It prints the site URL, the database name, the Node and pnpm versions, and the seeded administrator account: superadmin@example.com / 12345678.

At the end the installer starts the platform itself and polls http://localhost:3000 for up to a minute. If it reports the application may not have started, the startup output is in /tmp/bicrypto-startup.log.

The final screen offers systemctl start|stop|restart|status bicrypto. The same installer deliberately deletes /etc/systemd/system/bicrypto.service a few seconds earlier, because that unit re-ran pnpm start every 10 seconds and restarted the whole platform in a loop. Use pnpm start, pnpm stop and pnpm restart from the application root. Do not recreate the unit.

What it asks you

Defaults come from the current .env. The site URL is the public address — https://example.com, not a port. It is written to NEXT_PUBLIC_SITE_URL, which is compiled into the browser bundle and into the allowed image hosts, so changing it later means rebuilding the frontend.

Each defaults to the existing .env value; press Enter to keep it. An empty password is rejected three times before it offers to exit. The port is checked to be a number between 1 and 65535.

The connection test that runs after these prompts always reports success — the script tests the wrong exit status. A wrong password therefore looks fine here and surfaces later as a failed initial.sql import. If the import fails on access denied, your credentials were wrong all along.

Only asked if the target database already contains tables. Answer y and every table is dropped and re-imported; anything else skips the import and keeps what is there. On a re-run over a live install, answering y destroys your data.

Each is a "continue anyway?" prompt rather than a hard stop. Continuing past the Redis failure produces an install that cannot boot the backend — Redis is a hard dependency, not a nicety.

What it changes on the server

Worth knowing before you run it on a box that does other work:

  • Installs build tooling (build-essential/gcc, python3, curl, git, unzip) through your package manager.
  • Installs Node 26 from NodeSource, and pnpm globally via npm install -g pnpm@latest. Note the repository pins pnpm@10.28.0.
  • Installs and starts Redis, verifying it with redis-cli ping.
  • Creates .env from .env.example if absent, then writes your answers into it and generates four fresh 64-byte token secrets.
  • Imports initial.sql, installs dependencies into ./.pnpm-store, builds the frontend and runs pnpm seed.
  • If the application directory is owned by root, creates a bicrypto system user, chowns the tree to it and records APP_USER in .env.
  • Opens ssh, http, https and 3000 in ufw or firewalld. It never opens 4000, which is correct — see Ports.
  • Enables Apache proxy modules, or restarts nginx. It writes no vhost for either.
  • Runs pm2 startup so PM2 comes back after a reboot, but never runs pm2 save, so the saved process list is empty. Run pm2 save yourself once the three apps are up.

Repair flags

bash installer.sh --help              # list the flags
bash installer.sh --seed-only         # import initial.sql and seed, no build
bash installer.sh --fix-permissions   # re-apply ownership and file modes
bash installer.sh --fix-sharp         # rebuild the image library on older CPUs
bash installer.sh --clean-build       # delete build artefacts and node_modules

None of these run the OS detection first, so they do not check that you are root and they do not know your package manager — --fix-sharp in particular cannot install system libraries when invoked this way.

The next full install then runs pnpm install --frozen-lockfile against a missing lockfile and fails on its first attempt. If you use this flag, restore the lockfile from the release archive before reinstalling.

The manual path

Use this when the box is already provisioned, when you are installing under a configuration-management tool, or when the guided installer failed somewhere you would rather do by hand. It is the same sequence, without the package installation.

  1. Check the runtime. Any other major version fails at boot with Cannot find module './uws_linux_x64_115.node' or similar.

    node -v          # must be v22.x, v24.x or v26.x
    npm install -g pnpm@10.28.0
  2. Create .env from the template, then edit it. The shipped defaults point at a database called v4 on localhost as root with no password — all five database values need changing on a real box.

    cp .env.example .env
  3. Generate four token secrets. The values in .env.example are published samples; leaving them is the same as having no secret at all. The backend refuses any token secret shorter than 32 characters.

    node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

    Run it four times and paste the results into APP_ACCESS_TOKEN_SECRET, APP_REFRESH_TOKEN_SECRET, APP_RESET_TOKEN_SECRET and APP_VERIFY_TOKEN_SECRET.

  4. Create the database and import the schema. pnpm seed inserts rows into tables that initial.sql creates; running it first does nothing useful.

    mysql -u root -p -e "CREATE DATABASE \`bicrypto\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
    mysql -u root -p bicrypto < initial.sql
  5. Install dependencies from the application root. This is a pnpm workspace; do not run npm install in frontend/ or backend/.

    pnpm install
  6. Build the frontend. Finish editing .env before this step — every NEXT_PUBLIC_* value is compiled into the browser bundle here.

    pnpm build:frontend

    There is no backend build to run. backend/dist ships pre-built in the release archive. Never delete it: nothing in the install or update flow recreates it.

  7. Seed reference data. This writes roles, the 715 permission keys, notification templates and the first administrator.

    pnpm seed
  8. Start the platform.

    pnpm start
    pm2 save

The values that must be right

Everything else in .env can wait until the platform is up. These cannot.

The public address of the site. Compiled into the browser bundle and into the allowed image hosts at build time, so changing it later requires pnpm build:frontend. Also the sole source of the production CORS allowlist.
Must be production on a live install. It is what makes session cookies Secure, and what selects the production asset paths.
The five database values. The backend refuses to start if the host, user, password or name is missing, and the shipped defaults point at a database that does not exist on your server.
Where Redis lives. Unreachable Redis stops the boot with exit code 78 and a message naming the host and port it tried.
Leave unset for nginx or Apache on this machine — a forwarding header from a loopback connection is honoured automatically. Set to true ONLY for a load balancer on a different host, and understand that it then believes a forwarding header from any peer, including a caller who reaches the API port directly. Prefer TRUST_PROXY_CIDRS.
Networks other than loopback whose requests may carry a forwarding header — comma-separated addresses or CIDRs. This is the safe way to trust a proxy on another host, and it also unlocks the CDN headers (CF-Connecting-IP and friends), which are ignored by default because Apache and nginx forward them straight through from the client.

Two more are worth setting before the first pnpm seed, because they decide the first administrator account:

SUPERADMIN_EMAIL="you@example.com"
SUPERADMIN_PASSWORD="a-real-password"

Set neither and the seeder creates superadmin@example.com with the published password 12345678. The seeder records a marker in settings on its first run and never touches the user table again, so this is a one-time choice — after that, change the password from the profile screen.

Put a web server in front

Nothing you have done so far is reachable from the internet, and the frontend on its own cannot serve the API.

Two processes have to appear at one hostname: the frontend on localhost:3000 and the backend — including every WebSocket — on localhost:4000 under /api.

In development Next.js rewrites /api to the backend. In a production build it does not — the rewrite rules sit below an early return in frontend/next.config.js. If your proxy has no /api route, the site looks fine and every request inside it fails.

Apache is the default. The installer checks for it first: if apache2 or httpd is running it configures Apache and never looks at nginx. A Virtualmin box is Apache out of the box, which is what most installs are.

Note what the installer does and does not do. configure_apache() enables the proxy modules and restarts Apache — it does not write the vhost directives. configure_nginx() does nothing at all beyond restarting the service. Either way you add the proxy configuration yourself.

Apache

The default. Vhost directives, the modules to enable, and the Virtualmin caveat.

Nginx

The alternative, if you run nginx instead. Complete server block.

Every WebSocket in the platform lives under /api and connects to the page's own origin, so whichever server you use, handling the upgrade on that one prefix covers all of them. The server pings each socket every 30 seconds, so the proxy timeout must sit comfortably above that — a browser gives up permanently after five failed reconnects, so one aggressive timeout kills live prices for that tab until the user reloads.

HTTPS is not optional

In production the backend marks its accessToken and sessionId cookies Secure with SameSite=None. Browsers discard Secure cookies delivered over plain HTTP. A production install served over HTTP cannot log anyone in — the login request succeeds, the cookie is dropped, and the next request is anonymous. Nothing in the repository automates certificates; issue one with certbot or your host's tooling before you go further.

Ports and the firewall

The backend binds every interface in production, not just loopback. Ports 4000 and 4001 must not be reachable from outside the box — the proxy talks to them over 127.0.0.1. The installer opens 3000 in the firewall so you can test before DNS is ready; close it once the proxy works.

Sign in and activate the licence

Until the licence is activated, the backend answers almost every API route with 403 License validation failed. Please activate your license. A short list stays open precisely so you can get out of that state: /api/auth, /api/user/profile, /api/settings, /api/admin/system/license, /api/admin/system/extension and the geo-restriction routes. Everything else is closed.

  1. Open the site and sign in at /login with the administrator account — superadmin@example.com / 12345678 unless you set SUPERADMIN_EMAIL and SUPERADMIN_PASSWORD before seeding.

  2. Change that password immediately. It is published in this documentation, in the installer's own output, and in the seeder source.

  3. Go to /admin/system/license and enter your Envato purchase code. There is no menu entry for this screen; type the path, or follow the link from an exchange or extension detail page.

  4. Confirm the rest of the admin panel responds. If screens still return 403, activation did not take.

Activation and the hourly heartbeat call https://updates.mashdiv.com. If your firewall blocks outbound HTTPS to that host, activation fails outright, and an already-activated install keeps running on a 72-hour grace period before it starts refusing requests.

Check the install is healthy

Run these from the application root on the server.

pm2 list

You want exactly three online apps — backend, frontend and cron — with a restart count that is not climbing. A cron app that keeps restarting is usually memory pressure; it is capped at 2 GB on purpose.

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000
curl -s http://127.0.0.1:4000/api/settings | head -c 200

The first should print 200. The second should print a JSON object with settings and extensions keys — it is unauthenticated and licence-exempt, which makes it the right readiness probe for a proxy health check too. If the first works and the second is refused, the backend is down and the site will look alive while every action fails.

pm2 logs backend --lines 50

Exit code 78 in those logs is a configuration failure the backend will not retry: either the Node major version has no matching uWebSockets binary, or Redis is unreachable. The message says which. PM2 stops the app rather than crash-looping, so the explanation stays on screen.

Finally, load the site over its real domain in a browser, sign in, and open Admin → System → Settings. If that screen saves, the frontend, the API, the database, Redis and the licence are all working together.

If it fails

Two causes, and the log line names which. Either Node is not 22, 24 or 26 — check with node -v, and note that PM2 keeps whatever Node it was first started with, so pm2 kill and restart after upgrading Node — or Redis is unreachable. Test Redis with redis-cli -h 127.0.0.1 -p 6379 ping, which must answer PONG.

The licence is not activated, or the platform cannot reach https://updates.mashdiv.com. Sign in and open /admin/system/license; the licence routes are exempt from the gate, so that screen works even when nothing else does.

You are serving production over plain HTTP. Session cookies are Secure and are being discarded by the browser. Fix TLS at the proxy — there is no setting that turns this off.

Your database credentials were wrong. The installer's connection test cannot detect this, so it reported success and continued. Correct DB_USER, DB_PASSWORD, DB_HOST and DB_PORT in .env, import the schema by hand, then run bash installer.sh --seed-only.

Almost always the kernel OOM killer. Check dmesg | tail. The frontend build is the heaviest step in the whole install; add swap or build on a larger instance. On a CPU without AVX2 the build can also fail inside the image library instead — bash installer.sh --fix-sharp is the intended repair, but run it as root from the application root.

The installer applies chmod 644 to every file in the tree, which strips the execute bit from binaries in node_modules/.bin. Only *.sh files are restored. Reinstalling dependencies with pnpm install puts them back.

pm2 startup was configured but pm2 save was never run, so PM2 resurrected an empty list. Start the platform with pnpm start, then run pm2 save once.

That is what it does. pnpm stop stops the three apps and starts a maintenance server in their place, which answers 503 with a maintenance page for pages and 503 JSON for /api/*. It exits with an error rather than claiming success if a port is still held by something it does not manage. To close the ports entirely, use pnpm stop:all.

Everything the installer printed, including the parts that scrolled away, is in /var/log/bicrypto-installer.log. The startup attempt at the end writes to /tmp/bicrypto-startup.log.

Back up before you go live

Two things on this box cannot be recovered if you lose them:

  • .env — it holds ENCRYPTED_ENCRYPTION_KEY and ENCRYPTION_KEY_PASSPHRASE once you set up custodial wallets. Losing them means losing every encrypted wallet key on the platform. Nothing can rebuild them.
  • lic/ — the activated licence files, which are machine-bound.

The built-in backup at Admin → System → Database dumps MySQL only. Redis, .env, lic/ and frontend/public/uploads/ are yours to back up separately.