EmulatorJS Setup: Pokémon in Your Browser, 13 Steps [2026]

Every mainline Pokémon game from Red and Blue through Diamond and Pearl can now run inside a Chrome or Firefox tab, no installer, no admin rights, no separate app for every console generation. The tool behind that shift is EmulatorJS, an open-source project that compiles RetroArch cores to WebAssembly and drops them straight into a web page. Pair it with a self-hosted ROM manager and you get one URL that plays Game Boy, Game Boy Advance, Nintendo DS, and Nintendo 64 Pokémon titles on a phone, a Chromebook, or a work laptop where you can’t install anything at all.

This guide walks through a full setup, from picking a hosting path to tuning performance on weaker hardware. Budget 60 to 90 minutes for the first pass, most of it downloading ROM files you already own and letting Docker pull images.

The appeal isn’t just convenience. A single self-hosted URL means one library, one set of save files, and one place to pick up a Pokémon run wherever you left it, instead of juggling separate save states across a phone emulator, a laptop emulator, and whatever’s installed on a Steam Deck. For a broader look at how the native side of Pokémon emulation stacks up generation by generation, our Best Pokémon Emulators roundup covers the desktop and mobile apps this browser-based approach supplements rather than replaces.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why Play Pokémon Games in a Browser Instead of Installing an Emulator

Native emulators like Azahar or Eden are still the right call for 3DS and Switch-era Pokémon games, since EmulatorJS has no core for either platform, and neither does any browser-based project built on top of it. For 3DS titles specifically, Panda3DS is another native option worth knowing about if Azahar’s performance doesn’t suit your hardware. But for the four generations that came before them, GB, GBC, GBA, and NDS, plus the Nintendo 64 spin-offs, a browser-based setup solves a problem native apps can’t: access from any device without touching its file system. That matters on a school Chromebook, a locked-down work machine, a friend’s Xbox running Edge in kiosk mode, or a phone where you don’t want to hunt down an APK.

It also matters for anyone juggling multiple people in one household. A native emulator app ties its save files to whatever device it’s installed on. A browser-based setup ties save files to a login instead, so a kid finishing a Pokémon Crystal run on a tablet after school can pick the exact same save back up on the family PC that evening, without anyone emailing a .sav file around or plugging in a USB drive.

EmulatorJS itself is a small project by emulation standards, sitting around 4,200 stars on GitHub under a GPL-3.0 license, but it has become the default in-browser layer for a much larger ecosystem. Self-hosted ROM managers like RomM, Gaseous Server, and webЯcade all lean on it for the actual “press play and the game runs” moment, while EmulatorJS itself focuses purely on being a reliable frontend for WebAssembly-compiled RetroArch cores. That split of responsibilities, one project handles ROM libraries and metadata, another handles the emulation core, is what makes this setup worth the extra half hour compared to just installing mGBA.

There is a legal line worth stating up front. EmulatorJS and RomM are both legitimate open-source software. Downloading ROM files for games you do not own a physical or digital copy of is copyright infringement in most jurisdictions, and the BIOS files some cores need are copyrighted Nintendo firmware. Dump your own cartridges with hardware like a GBxCart RW, or your own DS and 3DS system files, rather than pulling either from a random site.

None of that legal caution changes what’s actually running under the hood once your library is built correctly. EmulatorJS itself never ships a ROM or a BIOS file, it’s a pure emulation engine, the same way mGBA or RetroArch ship no game data either. Everything playable in the setup below comes from files you supply, which is also why the setup process spends real time on library organization rather than a one-click “download and play” shortcut.

EmulatorJS vs Native Pokémon Emulators: When Browser-Based Makes Sense

Browser-based emulation isn’t a universal upgrade over installing a dedicated app, it’s a different trade-off. mGBA running natively on a desktop, as covered in our mGBA setup guide for Pokémon GBA games, will always squeeze out better accuracy and lower input latency than the same core compiled to WebAssembly and run inside a browser sandbox. What you give up in raw performance, you get back in reach: no install step, no platform-specific build, and a save file that follows your account instead of your device.

FactorNative Emulator (mGBA, Azahar, Eden)EmulatorJS in Browser
Install requiredYes, per device and OSNo, just a URL
Performance ceilingHigher, direct hardware accessLower, WebAssembly sandbox overhead
3DS / Switch supportYes (Azahar, Eden, Panda3DS)No, tops out at N64 and PSP
Cross-device save syncManual, via cloud storage or cableAutomatic, tied to your account
Works on locked-down devicesRarely, needs install permissionsYes, any modern browser

The practical takeaway: use this setup as a companion to native emulators, not a replacement for them. Keep Azahar or Eden installed for anything 3DS or Switch generation, and let a browser-based RomM instance handle the four generations that came before, where the performance gap is smallest and the convenience gain is biggest.

Prerequisites: What You Need Before You Start

This tutorial assumes a home server, NAS, or spare PC that stays on, since you’re standing up a small web service rather than a desktop app. Here’s the exact software and hardware baseline:

  • A Linux, Windows, or macOS machine with Docker Engine 27.0 or newer and the Docker Compose v2 plugin (bundled with Docker Desktop on Windows and Mac)
  • At least 2 GB of RAM free for the container stack and 10 GB of free disk space as a starting point for a Pokémon-focused library
  • A modern browser: Chrome 120+, Firefox 121+, or Safari 17+ on iOS, all with WebAssembly and WebGL2 enabled by default
  • Your own Pokémon ROM files, legally dumped from cartridges or carts you own, sorted by platform if possible
  • BIOS or firmware files for GBA and NDS emulation, again dumped from hardware you own
  • Optional but recommended: a domain name and a reverse proxy (Caddy, Nginx, or Traefik) for HTTPS, since some cores need a secure context to run at full speed
  • Optional: free IGDB API credentials from a Twitch Developer account, used only for cover art and metadata, not required to play games

None of this requires a beefy GPU. EmulatorJS cores for GB, GBA, and NDS run comfortably on a Raspberry Pi 4 or an old laptop repurposed as a home server. Nintendo 64 emulation is heavier and benefits from a real x86 CPU with a few dedicated cores.

On the ROM side, the cleanest path to a legally-dumped library is hardware you plug into your own cartridges, not a search engine. A device like a GBxCart RW, covered in our GBxCart RW setup guide, reads a Game Boy, Game Boy Color, or Game Boy Advance cartridge directly and writes out a clean ROM file, the same dump you’d get from any other flash cart reader. Nintendo DS titles typically get extracted with a flashcart designed for that purpose, and 3DS system files come from a console you own running a one-time dump tool. None of this article’s setup steps care where your ROMs came from technically, EmulatorJS just plays files, but building the library this way keeps you on the right side of copyright law.

Step 1: Pick a Hosting Path for Your Pokémon Library

Before writing a single Docker Compose file, decide how you want to serve EmulatorJS. LinuxServer.io retired its standalone linuxserver/emulatorjs Docker image on June 11, 2025 as part of a broader “spring cleaning” of its image catalog, and that image no longer receives updates or support. Anyone following an older tutorial that points at it is building on a dead end. LinuxServer’s own deprecation notice names three actively maintained replacements: RomM, Gaseous Server, and webЯcade.

PlatformBest ForEmulatorJS RoleSetup Difficulty
RomMFull ROM library management with metadata, cover art, and save syncBuilt in as the “Play” button, zero extra configModerate (needs a database container)
Gaseous ServerROM management with a stronger focus on multi-user librariesBuilt in, similar to RomMModerate
webЯcadeCloud-storage-backed libraries (Google Drive, Dropbox)Custom core wrapper around EmulatorJSModerate to advanced
Manual static hostingMinimalists who just want a single game page, no databaseLoaded directly via CDN script tagsSimple, but no library UI

This guide uses RomM as the primary path because it has the largest community, a healthy pace of releases, and a dedicated EmulatorJS integration page in its documentation that spells out per-console quirks. RomM’s own GitHub repository has passed 12,700 stars, and its README advertises browser play through EmulatorJS as requiring “no setup” once the library is scanned, though in practice a first-time Docker deployment still takes the steps below. If you already run RomM for a broader ROM collection spanning systems well beyond Pokémon, our RomM self-hosted setup guide covers the general library-management side in more depth than this Pokémon-focused walkthrough does. Step 12 below covers the manual, no-database route for anyone who wants the absolute minimum setup for a single Pokémon game page.

Step 2: Install Docker and Docker Compose

On Linux, install Docker Engine through your distribution’s package manager or the official convenience script, then confirm both Docker and Compose are present:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker
docker --version
docker compose version

On Windows or macOS, install Docker Desktop instead, which bundles Compose automatically. Log out and back in after adding your user to the docker group on Linux, otherwise every command below needs sudo.

Step 3: Deploy RomM With EmulatorJS Built In

RomM ships as a multi-container stack: the RomM app itself plus a MariaDB database for your library metadata. Create a project folder and a docker-compose.yml file:

mkdir -p ~/romm-pokemon && cd ~/romm-pokemon
mkdir -p library/roms/gb library/roms/gba library/roms/nds library/roms/n64
mkdir -p resources config

cat > .env << 'EOF'
DB_HOST=romm-db
DB_NAME=romm
DB_USER=romm
DB_PASSWD=CHANGEME_use_a_real_password
ROMM_AUTH_SECRET_KEY=CHANGEME_output_of_openssl_rand
ROMM_AUTH_USERNAME=admin
ROMM_AUTH_PASSWORD=CHANGEME_use_a_real_password
IGDB_CLIENT_ID=optional_leave_blank_to_skip_metadata
IGDB_CLIENT_SECRET=optional_leave_blank_to_skip_metadata
EOF

openssl rand -hex 32

Take the output of that last openssl rand -hex 32 command and paste it in as ROMM_AUTH_SECRET_KEY in the .env file. Now write the compose file itself:

services:
  romm:
    image: rommapp/romm:latest
    container_name: romm
    env_file: .env
    ports:
      - "8080:8080"
    volumes:
      - ./library:/romm/library
      - ./resources:/romm/resources
      - ./config:/romm/config
    depends_on:
      - romm-db
    restart: unless-stopped

  romm-db:
    image: mariadb:11
    container_name: romm-db
    environment:
      MARIADB_DATABASE: romm
      MARIADB_USER: romm
      MARIADB_PASSWORD: CHANGEME_use_a_real_password
      MARIADB_RANDOM_ROOT_PASSWORD: "true"
    volumes:
      - ./mariadb:/var/lib/mysql
    restart: unless-stopped

Save that file as docker-compose.yml in the same folder, then bring the stack up:

docker compose up -d
docker compose logs -f romm

Watch the logs until RomM reports it's listening, then open http://your-server-ip:8080 in a browser.

Step 4: First Login and Metadata Keys

Log in with the admin username and password you set in the .env file. RomM will prompt for IGDB credentials on first run. These pull cover art and box descriptions for your library and are entirely optional, skip that screen if you'd rather not create a free Twitch Developer account. Metadata makes browsing your Pokémon collection look better, it has no effect on whether games actually run.

Once logged in, head to Settings and confirm the platform list shows Game Boy, Game Boy Advance, Nintendo DS, and Nintendo 64. RomM auto-detects platforms from folder names inside your library directory, so if a platform is missing, double-check the folder naming from Step 3.

Step 5: Organize Your Pokémon ROMs by Platform

Copy your legally-owned ROM dumps into the matching folders you created earlier:

~/romm-pokemon/library/roms/
├── gb/    # Pokémon Red, Blue, Yellow, Gold, Silver, Crystal
├── gba/   # Ruby, Sapphire, Emerald, FireRed, LeafGreen
├── nds/   # Diamond, Pearl, Platinum, HeartGold, SoulSilver, Black, White
└── n64/   # Pokémon Stadium, Pokémon Stadium 2, Pokémon Snap

Back in the RomM web UI, click Rescan Library, or wait for the automatic scan on container restart. Each detected game shows up as a card with a Play button once the scan finishes. Pokémon Gold, Silver, and Crystal use the same Game Boy core as Red, Blue, and Yellow, since they're Game Boy Color titles that fall under the same "gb" platform bucket in EmulatorJS.

Step 6: Upload BIOS and Firmware Files

Some cores need BIOS or system firmware to boot correctly, especially the Nintendo DS core for Diamond, Pearl, and their siblings. RomM's documentation is specific about this: bundle every required BIOS file for a platform into a single ZIP archive, then upload it through the Firmware tab on that platform's detail page. EmulatorJS automatically detects the bundle and applies the right file to the right core, you don't have to rename anything by hand.

Game Boy and Game Boy Advance titles generally run BIOS-free through the gambatte and mgba cores, so this step matters most for your NDS folder. Only use firmware you extracted yourself from a DS console you own, following the same rule as the ROMs themselves.

Step 7: Launch Your First Game and Pick the Right Core

Click Play on any Pokémon title and EmulatorJS launches full-screen inside the browser tab. Each system maps to a default core, with fallbacks available if the default misbehaves on your hardware:

SystemDefault CoreAlternative CorePokémon Titles
Game Boy / Game Boy ColorgambatteNone neededRed, Blue, Yellow, Gold, Silver, Crystal
Game Boy AdvancemgbaNone neededRuby, Sapphire, Emerald, FireRed, LeafGreen
Nintendo DSmelondsdesmume2015, desmumeDiamond, Pearl, Platinum, HeartGold, SoulSilver, Black, White
Nintendo 64mupen64plus_nextparallel-n64 (default on iOS)Pokémon Stadium, Stadium 2, Snap

If melonds stutters on a specific Nintendo DS Pokémon title, open the in-game menu with the F1 key on desktop, go to Core Options, and switch to desmume2015 as a fallback. On iPhone and iPad, EmulatorJS already defaults Nintendo 64 to parallel-n64 rather than mupen64plus_next, since Apple's WebKit engine has historically struggled with the more accurate but heavier core.

Which Pokémon Games Play Best on Each Core

Not every Pokémon title behaves the same way once it's running through a WebAssembly core, and a few generations have quirks worth knowing before you assume something is broken. Red, Blue, Yellow, Gold, Silver, and Crystal are the easiest tier, the gambatte core is mature, fast, and rarely needs any tuning at all, even on a Raspberry Pi acting as your RomM host.

Ruby, Sapphire, Emerald, FireRed, and LeafGreen run through mgba and are just as reliable, though anyone chasing perfect link-cable trading between two GBA sessions should know that feature depends on netplay rather than the old physical link cable protocol, and works best treated as a co-op curiosity rather than a full replacement for real link trades.

The Nintendo DS generation is where things get more particular. Diamond, Pearl, and Platinum use the touch screen sparingly, mostly for the in-game clock and a handful of menus, and EmulatorJS maps mouse clicks or touch input to that lower screen without extra configuration. HeartGold and SoulSilver lean on the Pokéathlon and the Pokéwalker-adjacent menus more heavily, and while the core handles the touch input fine, players used to a stylus report the mouse-pointer feel takes a few minutes to adjust to. Black and White's dynamic sprite animations are the most demanding DS-era Pokémon games for the melonds core, so if you notice slowdown anywhere in this generation, that's usually where it shows up first.

Pokémon Stadium, Stadium 2, and Snap on the N64 core are the heaviest of the bunch by a wide margin, both games were built around the Transfer Pak accessory that let the original hardware read cartridges directly, a feature with no browser equivalent. In EmulatorJS these run as standalone N64 titles without cartridge-transfer minigames, which is a known limitation rather than a misconfiguration on your end.

Step 8: Configure Controls, Save States, and Cross-Device Sync

EmulatorJS maps keyboard controls automatically and also detects USB and Bluetooth gamepads through the standard Gamepad API, no extra driver needed on desktop Chrome or Firefox. Press F1 during gameplay to open the RetroArch-style quick menu, where you can remap buttons, adjust save slots, and toggle fast-forward for grinding through repetitive Pokémon battles.

Because RomM sits between the browser and your ROM library, any save file or save state you create inside a session syncs back to the server automatically, no manual export needed. That's the practical payoff of this whole setup: start a Pokémon Emerald run on a work laptop at lunch, close the tab, and pick the same save back up on a phone that evening by logging into the same RomM URL.

Step 9: Put HTTPS in Front of Your Server

Some EmulatorJS features, especially PlayStation Portable emulation through the ppsspp core, depend on the browser's SharedArrayBuffer API, which only works in a secure, cross-origin-isolated context. None of the classic Pokémon consoles strictly require this, but running your whole stack behind HTTPS is good practice regardless, and it unlocks netplay in the next step without browser warnings. A minimal Caddy reverse proxy config looks like this:

pokemon.yourdomain.com {
    reverse_proxy localhost:8080
}

Caddy handles the TLS certificate automatically through Let's Encrypt as long as your domain's DNS points at the server and ports 80 and 443 are open. Restart Caddy and the RomM instance is reachable over HTTPS at that subdomain.

Step 10: Turn On Netplay for Co-Op Trading and Battling

EmulatorJS supports netplay for up to four connected players, configured through the emulatorjs.netplay flag in RomM's configuration file along with a set of ICE servers for connection negotiation. This works best for turn-based interactions, trading Pokémon between two Game Boy sessions or running a link battle, rather than frame-perfect action games where lag matters more. Enabling netplay occasionally surfaces 404 errors tied to the nightly core CDN, which the project's own documentation notes usually clears up on the next update rather than pointing to a config mistake on your end.

A minimal netplay configuration snippet, added to RomM's config file, points EmulatorJS at a public STUN server so two players behind different home routers can find each other without either one manually forwarding ports:

emulatorjs:
  netplay: true
  iceServers:
    - urls: "stun:stun.l.google.com:19302"

Both players still need to be looking at the same game session inside RomM and share a session code generated when the host starts netplay from the in-game quick menu. If a connection won't establish behind stricter corporate or campus firewalls, that's typically a NAT traversal issue no amount of ICE server tweaking fixes, and the practical answer is to try from a home network instead.

Step 11: Tune Performance Per Console

Game Boy and Game Boy Advance cores are light enough to run on almost anything with a modern browser, including a Raspberry Pi acting as the RomM host. Nintendo DS and especially Nintendo 64 are heavier, since both consoles push more polygons and audio channels than WebAssembly handles for free. RomM's own documentation flags this directly: older or less powerful devices can struggle, particularly with demanding cores, and the fix is usually to test an alternative browser or switch to the lighter alternative core listed in the table above rather than assuming the ROM itself is bad.

If Pokémon Stadium chugs on the N64 core, drop the internal resolution in Core Options before touching anything else, that single change recovers more frame rate than any browser setting.

Step 12: The Manual Route: Self-Hosting Without RomM

If you don't want a database, a library UI, or user accounts, and just need one Pokémon game embedded in an existing web page, EmulatorJS can be loaded directly from its own CDN with a handful of script tags. This is the same approach the project's Quick Start documentation walks through for developers embedding the player in their own sites:

<div id="game"></div>
<script>
  EJS_player = "#game";
  EJS_gameUrl = "/roms/pokemon-emerald.gba";
  EJS_core = "gba";
  EJS_pathtodata = "https://cdn.emulatorjs.org/latest/data/";
  EJS_gameName = "Pokemon Emerald";
</script>
<script src="https://cdn.emulatorjs.org/latest/data/loader.js"></script>

Host that HTML file plus your ROM on any static web server, Nginx, a GitHub Pages repo, or a plain Python http.server for local testing, and it loads the same way a full RomM deployment would, just without library management, metadata, or cross-device save sync. This route suits a one-off Pokémon Randomizer run more than a full personal library.

Common Pitfalls When Setting Up a Pokémon Browser Emulator

  • Following an old tutorial that uses linuxserver/emulatorjs. That image was deprecated on June 11, 2025 and no longer pulls the latest tag. Use RomM, Gaseous Server, or webЯcade instead.
  • Renaming ROM files instead of matching folder platforms. RomM sorts by the folder a ROM sits in (gb, gba, nds, n64), not by file extension guesswork. A Pokémon DS ROM dropped in the gba folder will fail to launch or launch the wrong core.
  • Skipping the BIOS ZIP for Nintendo DS titles. Diamond and Pearl will often show a black screen or crash on launch without the correct firmware bundle uploaded through the Firmware tab.
  • Running everything over plain HTTP and then wondering why netplay fails. Several EmulatorJS features assume a secure context. Put a reverse proxy with a real TLS certificate in front of the stack early rather than retrofitting it later.
  • Assuming Nintendo 64 will run as smoothly as Game Boy Advance. The mupen64plus_next core is significantly heavier than gambatte or mgba. Budget real CPU headroom on the host if you plan to play Pokémon Stadium regularly.

Troubleshooting

  • RomM container exits immediately after docker compose up. Check docker compose logs romm, this is almost always a missing or malformed .env value, most often ROMM_AUTH_SECRET_KEY left blank.
  • Database connection refused on first boot. MariaDB takes a few seconds to initialize on a fresh volume. Add a healthcheck and depends_on condition, or simply restart the romm service after 30 seconds.
  • Games show up in the library but Play does nothing. Open the browser console (F12) and look for a blocked WebAssembly or CORS error, usually caused by an ad blocker or a reverse proxy stripping headers.
  • Nintendo DS games boot to a black screen. Missing or incorrectly bundled BIOS files. Re-check the ZIP structure against RomM's Firmware tab requirements.
  • Save states don't carry over between devices. Confirm you're logging into the same RomM account on both devices, saves are tied to your user profile, not the browser's local storage.
  • Netplay session won't connect between two players. ICE server configuration is likely missing or unreachable behind NAT. Add a public STUN server to the netplay config as a first fix.
  • Nintendo 64 audio crackles or drops out. Lower the audio buffer size in Core Options, or switch from mupen64plus_next to parallel-n64 for a lighter but slightly less accurate alternative.
  • Safari on iOS refuses to load a core. Confirm you're on Safari 17 or newer and that the site is served over HTTPS. Older iOS Safari versions have inconsistent WebAssembly threading support.

Advanced Tips

Pin your EmulatorJS core version rather than always pulling latest once your Pokémon library is stable. EmulatorJS ships frequent point releases, and a 4.3.0-pre branch was already circulating in pre-release testing by May 2026 with a full ES6 module migration and a new dosbox_pure core, changes that are great for the project long-term but not something you want landing unannounced on a server your family uses to play Pokémon Crystal.

EmulatorJS VersionStatusNotable Change
4.2.2StableAdded dosbox_pure core, save state improvements
4.2.3Stable, current defaultBug-fix release, no core changes
4.3.0-prePre-release, not marked stableES6 module migration, new cores

Run your Docker volumes on an SSD rather than spinning storage if your library grows past a few dozen Pokémon ROMs with cover art and save states attached, RomM's database does a fair amount of small, random reads during library scans. And if you're serving this to a household rather than just yourself, create separate RomM user accounts per person, since save states and cloud sync are tied to the logged-in user, not the device.

If you're exposing the RomM instance to the internet rather than keeping it on a home network, put it behind a proper firewall rule set and consider a VPN or a Cloudflare Tunnel instead of forwarding ports directly. A public-facing ROM library with no additional access control is a bigger attack surface than most people realize, since it's both a web app with a login form and a file server holding whatever else you've added to the library over time.

For anyone who wants EmulatorJS cores to load instantly rather than pulling from the public CDN on every session, mirror the core files locally by pointing EJS_pathtodata at a folder on your own server instead of cdn.emulatorjs.org. This adds a manual update step whenever a new EmulatorJS release ships, but it removes a dependency on an external CDN staying online, which matters if you're running this setup somewhere with unreliable internet.

Keeping Your Setup Updated: Backups and Version Pinning

A self-hosted Pokémon library is a small piece of infrastructure, and like any infrastructure it needs a maintenance habit, not just a one-time setup. Three things are worth backing up on a regular schedule: the mariadb folder holding your library database, the resources folder holding cover art and metadata, and the library folder holding the actual ROM files and any BIOS bundles you uploaded. A simple weekly cron job that tars all three into a dated archive on a separate drive covers the realistic worst case, a failed disk or a botched update, without much effort.

0 3 * * 0 tar -czf /backups/romm-$(date +%F).tar.gz \
  ~/romm-pokemon/mariadb ~/romm-pokemon/resources ~/romm-pokemon/library

On the update side, treat RomM and EmulatorJS as two separate things to track. Updating the RomM container with docker compose pull && docker compose up -d brings in whatever EmulatorJS core version RomM has bundled with that release, which is usually a stable point release rather than a pre-release branch. If you're loading EmulatorJS manually through the CDN approach from Step 12, you're pulling directly from the latest folder on cdn.emulatorjs.org, which means a new EmulatorJS release, stable or pre-release, reaches your page the moment it ships. Pin to a dated or versioned path instead of latest if that unpredictability bothers you, at the cost of missing bug fixes until you manually bump the version.

The Complete Working Project, Recapped

By this point you have a Docker Compose stack running RomM and MariaDB, a library folder split into gb, gba, nds, and n64 subfolders holding your own legally-dumped Pokémon ROMs, BIOS files bundled and uploaded for Nintendo DS titles, HTTPS handled by a reverse proxy, and netplay configured for co-op trading. That's a self-hosted, browser-accessible Pokémon library that any device on your network, or the internet if you choose to expose it, can reach through a single URL with no client software to install.

From here, the natural next step for most people is layering in RetroAchievements support if RomM's version supports it, or expanding the library folders to cover non-Pokémon Game Boy and DS games using the exact same core mappings covered above.

It's worth being honest about what this setup doesn't replace. If your goal is squeezing every frame of performance out of Pokémon Emerald with custom shaders and cheat codes, a native mGBA install will always beat a browser tab running the same core through WebAssembly. What this project buys you instead is reach: the same Pokémon library reachable from a laptop, a phone, a tablet, or a borrowed computer at a friend's house, with saves that follow your login rather than a specific device's file system. For a lot of casual and nostalgia-driven replays, that trade is worth making.

Frequently Asked Questions

Can EmulatorJS run 3DS or Switch Pokémon games?
No. EmulatorJS's core library tops out at Nintendo 64 and PlayStation Portable among Nintendo-adjacent handhelds. For Pokémon X/Y, Sun/Moon, or Legends Z-A you need a native emulator like Azahar or Eden instead.

Is RomM the only way to run EmulatorJS?
No. Gaseous Server and webЯcade both build on EmulatorJS as well, and Step 12 above covers loading it manually with no ROM manager at all for a single-game page.

Why did my old EmulatorJS Docker tutorial stop working?
It likely referenced the linuxserver/emulatorjs image, which LinuxServer.io deprecated on June 11, 2025. Deprecated images can no longer pull the latest or nightly tags, only pinned version tags, which is why old compose files suddenly break.

Do I need a powerful server to run this?
For Game Boy, Game Boy Color, and Game Boy Advance Pokémon titles, no, a Raspberry Pi 4 handles it fine. Nintendo DS and especially Nintendo 64 titles want a real CPU with a few dedicated cores for smooth playback.

Can I play with a friend over the internet?
Yes, through EmulatorJS's netplay feature, which supports up to four players and works best for turn-based interactions like trading or link battles rather than fast action games.

Are BIOS files legal to use?
BIOS and system firmware files are copyrighted. Only use ones you extracted yourself from hardware you own. Downloading pre-dumped BIOS files from third-party sites is a copyright violation regardless of how common the practice is in emulation communities.

Does this work on mobile phones?
Yes. EmulatorJS runs in mobile Chrome and Safari, with on-screen touch controls generated automatically when no gamepad is detected. iOS defaults Nintendo 64 emulation to the lighter parallel-n64 core rather than mupen64plus_next.

What happens to my save files if I switch servers later?
RomM stores saves and save states in its own database and resources volume, separate from the raw ROM files. Back up the resources and mariadb folders from your Docker Compose directory and you can restore the same library, saves included, on a new host.

Can I run this on a Synology or QNAP NAS instead of a spare PC?
Yes, as long as the NAS supports Docker or Container Manager, which most models from the last several years do. Resource requirements stay modest for GB, GBC, and GBA libraries, though Nintendo 64 titles will feel the difference on a NAS's lower-power CPU compared to a dedicated home server.

Does using EmulatorJS through RomM cost anything?
RomM, MariaDB, and EmulatorJS are all free and open source. The only recurring cost, if you choose to take one on, is a domain name for HTTPS access from outside your home network, typically a few dollars a year, and whatever electricity the always-on server draws.

Related Coverage

Elias Virtanen

Elias Virtanen

Cybersecurity Analyst

Elias Virtanen is the Cybersecurity Analyst at Tech Insider, bringing hands-on expertise from his background in penetration testing and security consulting. He previously worked as a security researcher at F-Secure in Helsinki, where he focused on threat intelligence and vulnerability disclosure. Elias covers ransomware trends, zero-trust architecture, and the evolving regulatory landscape including NIS2 and the EU Cyber Resilience Act. He holds a CISSP certification and an MSc in Information Security from Aalto University.

View all articles