Applications Azure

JBrowse 2 on Ubuntu 24.04 on Azure User Guide

| Product: JBrowse 2 on Ubuntu 24.04 LTS on Azure

Overview

This image runs JBrowse 2 v4.3.0, the open source genome browser from the Generic Model Organism Database (GMOD) project, on Ubuntu 24.04 LTS. JBrowse 2 is a fast, modern browser for genomic data: it renders reference sequences, gene and feature annotation, read alignments, variants, quantitative data and whole-genome synteny, and it streams straight out of standard indexed bioinformatics containers such as BAM, CRAM, VCF, bigWig and tabix-indexed GFF3 and BED.

The bundled E. coli K-12 MG1655 demo genome rendering on first visit: named RefSeq genes, a full Illumina read pileup with coverage, and a called variant

Two things make this more than a folder of static files. First, the VM ships with a complete, real demo genome already configured and rendering, so it is useful the moment it boots rather than after a day of data wrangling. Second, it carries an authenticated admin surface - the JBrowse admin server, reached through a password-protected nginx vhost on port 8080 - so you add assemblies and tracks through the browser and JBrowse writes the changes back to its own configuration.

The data plane is set up the way JBrowse actually needs it. Indexed genomic formats are read with HTTP range requests, and a host that does not answer them correctly gives you an empty browser with no error message. nginx here serves the data directory with byte-range support, compression disabled on the already-compressed containers, and the image is validated at build time against real 206 Partial Content responses.

What is included:

  • JBrowse 2 v4.3.0, the official pre-built JBrowse Web release, pinned by sha256 and served from /var/www/jbrowse
  • A ready-to-use demo genome: E. coli K-12 MG1655 (NCBI RefSeq GCF_000005845.2, NC_000913.3) with four real tracks - reference sequence, NCBI RefSeq gene annotation, Illumina MiSeq read alignments from ENA run SRR1770413, and variants called from those alignments
  • A trix name-search index, so the location box resolves gene names and locus tags such as thrA or b0002
  • nginx on port 80 serving the browser and the data with byte-range support, plus an unauthenticated /healthz endpoint for Azure Load Balancer probes
  • A second nginx vhost on port 8080 carrying HTTP Basic authentication with a per-VM credential, in front of the JBrowse admin server
  • The @jbrowse/cli command line tool installed globally, for jbrowse add-assembly, add-track and text-index
  • Per-VM credentials written to /root/jbrowse-credentials.txt on first boot - nothing is baked into the image
  • A fully patched Ubuntu 24.04 LTS base with unattended security upgrades enabled, and Node.js 22 LTS from NodeSource so the JavaScript runtime keeps receiving updates
  • 24/7 cloudimg support

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is the recommended size and is comfortable: with every service running and the demo genome loaded, the appliance uses well under 600 MB of RAM, because JBrowse does its rendering in the visitor's browser and the server's job is to stream byte ranges. The image occupies roughly 3 GB on the OS disk, of which about 81 MB is JBrowse and its demo dataset, so the default 30 GB OS disk leaves plenty of room for your own genomes.

NSG inbound: allow 22/tcp from your management network, 80/tcp for the genome browser, and 8080/tcp from your management network only for the admin surface. The appliance serves plain HTTP; for production or shared access, terminate TLS in front of it with your own domain using a reverse proxy or Azure Application Gateway.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for JBrowse 2 by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create then Create. Add an inbound rule for 8080/tcp, scoped to your own address range, once the VM is up.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name jbrowse \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

az vm open-port --resource-group <your-rg> --name jbrowse --port 80 --priority 1010
az vm open-port --resource-group <your-rg> --name jbrowse --port 8080 --priority 1020

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services are running

Four units make up the appliance. jbrowse-firstboot.service mints this VM's own admin credential before anything is published; nginx.service serves the browser on port 80 and the authenticated admin vhost on port 8080; jbrowse-admin.service runs the JBrowse admin server on loopback; and jbrowse-postboot.service publishes the credentials once the admin server has minted its key.

systemctl is-active nginx.service jbrowse-admin.service jbrowse-firstboot.service jbrowse-postboot.service

All four report active:

active
active
active
active

Check what is listening. nginx holds 80 and 8080; the admin server holds 9090, and although the process binds all interfaces, systemd's address filter confines it to loopback (proved in Step 8):

sudo ss -lntp | grep -E ':80 |:8080 |:9090 '
LISTEN 0  511   0.0.0.0:80     0.0.0.0:*   users:(("nginx",pid=5533,fd=5),("nginx",pid=5532,fd=5))
LISTEN 0  511   0.0.0.0:8080   0.0.0.0:*   users:(("nginx",pid=5533,fd=7),("nginx",pid=5532,fd=7))
LISTEN 0  511      [::]:80        [::]:*   users:(("nginx",pid=5533,fd=6),("nginx",pid=5532,fd=6))
LISTEN 0  511         *:9090         *:*   users:(("node",pid=5531,fd=21))
LISTEN 0  511      [::]:8080      [::]:*   users:(("nginx",pid=5533,fd=8),("nginx",pid=5532,fd=8))

nginx, the JBrowse admin server and both first boot units all active, with the pinned JBrowse and Node versions

Step 5 - Read your per-VM credentials

The genome browser itself has no login - it is a read-only viewer of the data you choose to publish, and JBrowse ships no account model. Everything that can change the appliance sits behind the admin surface on port 8080, whose password is generated on this VM at first boot and written to a root-only file:

sudo sed -e 's/^\(JBROWSE_ADMIN_PASSWORD=\).*/\1****************/' -e 's/^\(JBROWSE_ADMIN_KEY=\).*/\1**********/' /root/jbrowse-credentials.txt
# JBrowse 2 appliance by cloudimg - per-VM access details (generated on first boot)
#
# The genome browser itself is a READ-ONLY viewer and is published without a login: anyone
# who can reach port 80 can look at the genomes you publish. Everything that can CHANGE the
# appliance - adding assemblies and tracks, editing config.json - lives behind the
# authenticated admin surface on port 8080 with the credentials below.
#
# The admin key is re-minted by jbrowse-admin.service on every start; this file is kept in
# step automatically. To read the live value: sudo cat /var/lib/jbrowse/admin-key
JBROWSE_URL=http://<vm-public-ip>/
JBROWSE_ADMIN_URL=http://<vm-public-ip>:8080/
JBROWSE_ADMIN_USER=jbadmin
JBROWSE_ADMIN_PASSWORD=****************
JBROWSE_ADMIN_KEY=**********

Drop the sed filter to read the real values. The file is mode 0600, owned by root, and its contents are unique to this VM - the captured image contains no password, no admin key and no htpasswd entry.

The per VM access details written at first boot, with the admin password and admin key masked, alongside the bcrypt htpasswd entry and the admin server's loopback confinement

Step 6 - Open the genome browser

Browse to http://<vm-public-ip>/ and JBrowse opens straight onto the bundled genome, with the annotation, alignment and variant tracks already loaded - no configuration, no file uploads. The screenshot at the top of this guide is that first view: named E. coli genes such as thrA, dnaK and nhaA drawn from the NCBI RefSeq annotation, a full Illumina read pileup with its coverage histogram, and a called variant rendered on the variants track.

Confirm the same thing from the command line - the health endpoint, and the configuration JBrowse loads:

curl -s -o /dev/null -w 'healthz: %{http_code}\n' http://127.0.0.1/healthz
curl -s http://127.0.0.1/config.json | python3 -c "import json,sys; c=json.load(sys.stdin); print('assembly:', c['assemblies'][0]['displayName']); [print('track:  ', t['name'], '  [', t['trackId'], ']') for t in c['tracks']]; print('default view:', c['defaultSession']['views'][0]['init']['loc'])"
healthz: 200
assembly: E. coli K-12 MG1655 (NCBI RefSeq GCF_000005845.2)
track:   NCBI RefSeq genes   [ ecoli_refseq_genes ]
track:   Illumina MiSeq alignments (SRR1770413)   [ ecoli_srr1770413_alignments ]
track:   Variants called from SRR1770413   [ ecoli_srr1770413_variants ]
default view: NC_000913.3:1..25000

Step 7 - Explore the demo genome

Type a region into the location box at the top of the view - for example NC_000913.3:3000..5500 - and JBrowse redraws at that scale. At a few kilobases the alignment track resolves into individual reads, colour-coded where a read disagrees with the reference, with the coverage histogram above it.

The Illumina read pileup, coverage histogram and mismatched bases drawn across a 2.5 kb window

Keep zooming - NC_000913.3:4200..4360 - and you reach base resolution, where every read's insertions and individual mismatched bases are drawn.

Base level zoom showing individual read mismatches and insertions across a 160 bp window

Because the image ships a trix name-search index, the location box also accepts gene names and locus tags: type thrA and JBrowse jumps to that gene. The underlying data is real and you can interrogate it directly on the VM:

sudo tabix /var/www/jbrowse/data/ecoli/ecoli_K12_MG1655.sorted.gff.gz NC_000913.3:1-6000 | awk -F'\t' '$3=="gene"' | cut -f1,3,4,5 | head -5
sudo samtools idxstats /var/www/jbrowse/data/ecoli/SRR1770413.sorted.bam | head -2
echo -n "variants called: "; sudo bcftools view -H /var/www/jbrowse/data/ecoli/SRR1770413.variants.vcf.gz | wc -l
NC_000913.3 gene    190 255
NC_000913.3 gene    337 2799
NC_000913.3 gene    2801    3733
NC_000913.3 gene    3734    5020
NC_000913.3 gene    5234    5530
NC_000913.3 4641652 212220  1738
*   0   0   86322
variants called: 349

That is 212,220 reads mapped across the 4,641,652 bp chromosome and 349 variant calls, all generated from public archive data at build time.

The bundled demo genome on disk: the indexed reference, the tabix indexed RefSeq annotation, the sorted BAM and the called variants

Step 8 - Byte ranges, admin authentication and loopback confinement

JBrowse reads BAM, CRAM, bigWig and tabix files by fetching small byte ranges rather than downloading whole files, so range support is not optional - without it the browser silently renders nothing. Confirm the server answers with 206 Partial Content:

curl -sI -H 'Range: bytes=0-1023' http://127.0.0.1/data/ecoli/SRR1770413.sorted.bam | grep -iE '^(HTTP|Content-Range|Content-Length)'
HTTP/1.1 206 Partial Content
Content-Length: 1024
Content-Range: bytes 0-1023/48800773

The admin vhost accepts only this VM's credential. Anonymous requests and common default passwords are refused:

ADMIN_PW=$(sudo sed -n 's/^JBROWSE_ADMIN_PASSWORD=//p' /root/jbrowse-credentials.txt)
curl -s -o /dev/null -w 'no credentials     : %{http_code}\n' http://127.0.0.1:8080/
curl -s -o /dev/null -w 'admin:password     : %{http_code}\n' -u admin:password http://127.0.0.1:8080/
curl -s -o /dev/null -w 'this VM credential : %{http_code}\n' -u "jbadmin:${ADMIN_PW}" http://127.0.0.1:8080/
no credentials     : 401
admin:password     : 401
this VM credential : 200

The admin server behind that vhost is a small Node service that binds all interfaces and offers no option to bind loopback, so the image confines it with systemd's address filter instead. It is reachable from 127.0.0.1 and refused everywhere else:

MYIP=$(hostname -I | awk '{print $1}')
curl -s -o /dev/null -m 5 "http://${MYIP}:9090/" && echo "REACHABLE off loopback - unexpected" || echo "refused on ${MYIP}:9090, as designed"
curl -s -o /dev/null -w 'on 127.0.0.1:9090 : %{http_code}\n' -m 10 http://127.0.0.1:9090/config.json
sudo systemctl show jbrowse-admin.service -p IPAddressAllow -p IPAddressDeny
refused on 10.0.0.10:9090, as designed
on 127.0.0.1:9090 : 200
IPAddressAllow=::1/128 127.0.0.0/8
IPAddressDeny=0.0.0.0/0 ::/0

Byte range 206 responses, the admin vhost rejecting anonymous and default credentials while accepting this VM's own, and the admin server refusing connections off loopback

Step 9 - Use the admin surface

Browse to http://<vm-public-ip>:8080/?adminKey=<admin-key>, using JBROWSE_ADMIN_USER and JBROWSE_ADMIN_PASSWORD from your notes at the browser's authentication prompt and the JBROWSE_ADMIN_KEY value as the adminKey query parameter. JBrowse loads in admin mode - note the extra ADMIN menu in the top bar, which is absent from the public view on port 80. From there you can open the assembly manager, add and edit tracks, and set the default session; JBrowse writes every change back to /var/www/jbrowse/config.json through the admin server.

The password protected admin surface running JBrowse in admin mode, with the extra ADMIN menu that the public view does not have

The admin key is regenerated every time jbrowse-admin.service starts, and /root/jbrowse-credentials.txt is kept in step automatically. If you restart the service, read the current value with:

sudo cat /var/lib/jbrowse/admin-key

Step 10 - Load your own genomes and tracks

The @jbrowse/cli tool is installed globally and is the scriptable route for loading your own data. Confirm it and the search index that ships with the demo genome:

jbrowse --version
ls -1 /var/www/jbrowse/trix/
@jbrowse/cli version 4.3.0
ecoli_K12_MG1655.ix
ecoli_K12_MG1655.ixx
ecoli_K12_MG1655_meta.json

Copy your files under the served data directory, then register them. Reference sequences should be bgzip compressed with .fai and .gzi indexes, annotation should be position sorted and tabix indexed, and alignments and variants need their .bai or .tbi alongside:

sudo mkdir -p /var/www/jbrowse/data/myorg
sudo cp mygenome.fa.gz mygenome.fa.gz.fai mygenome.fa.gz.gzi /var/www/jbrowse/data/myorg/

cd /var/www/jbrowse
sudo jbrowse add-assembly data/myorg/mygenome.fa.gz \
  --out /var/www/jbrowse --load inPlace --type bgzipFasta \
  --name myorg --displayName "My organism (v1)"

sudo jbrowse add-track data/myorg/mygenes.sorted.gff.gz \
  --out /var/www/jbrowse --load inPlace \
  --trackId myorg_genes --name "My gene models" \
  --assemblyNames myorg --category Annotation

sudo jbrowse text-index --out /var/www/jbrowse --tracks myorg_genes --force
sudo chown jbrowse:jbrowse /var/www/jbrowse/config.json

Reload the browser and the new assembly appears in the assembly selector. If you need to prepare the indexes first, samtools, bcftools, tabix, bgzip and minimap2 are all installed on the image.

Step 11 - Footprint

The appliance is deliberately light, because rendering happens client side:

free -m | head -2
df -h / | tail -1
sudo du -sh /var/www/jbrowse
               total        used        free      shared  buff/cache   available
Mem:            3912         537        2058           3        1573        3374
/dev/root        29G  3.1G   25G  11% /
81M /var/www/jbrowse

Security notes

The genome browser on port 80 is public by design and has no login: JBrowse is a viewer, and everything it serves is data you have chosen to publish. Treat port 80 and the /data/ directory as world readable, and only place data there that you are content to publish. If you need access control, put the VM behind a reverse proxy, Azure Application Gateway or Front Door that enforces your own authentication, and restrict 80/tcp in the NSG to the proxy.

Port 8080 is different: it is the only way to change the appliance, and it is protected by an HTTP Basic credential generated on this VM at first boot and stored as a bcrypt hash. Restrict 8080/tcp in the NSG to your management network, and keep /root/jbrowse-credentials.txt confidential. Because the admin vhost carries a password over plain HTTP, put TLS in front of it before using it across an untrusted network.

The upstream JBrowse release archive also contains the project's own test fixtures, which embed third-party OAuth application identifiers. Those are removed from this image, and the build fails closed if they reappear.

Troubleshooting

If the browser loads but every track is empty, the usual cause is a data host that does not honour range requests - re-run the curl -sI -H 'Range: ...' check in Step 8 and confirm you see 206 Partial Content. If the page does not load at all, check systemctl status nginx jbrowse-admin and journalctl -u jbrowse-firstboot -u jbrowse-postboot: both first-boot units are one-shot and their logs show exactly what was minted. If the admin surface returns 401 with what you believe are the right credentials, re-read them with sudo cat /root/jbrowse-credentials.txt; if it returns 502, the admin server is not running - sudo systemctl restart jbrowse-admin and re-read the admin key.

Support

This image is supported 24/7 by cloudimg. For help with deployment or configuration, contact support through cloudimg.co.uk.