Deploying Web-based Retro Game Emulator Using Docker Based on emulatorjs
本文最后更新于 159 天前,其中的信息可能已经有所发展或是发生改变,如有失效可到评论区留言。
Article Abstract
Deploy emulatorjs via Docker to build a Web-based retro game emulator, supporting the running of ROMs from various game consoles. The solution includes creating configuration directories, mapping ROM files, managing user accounts and configuration files, and configuring public network access. After deployment, games can be accessed via a browser, supporting multi-platform ROM loading and key customization, though the default keyboard mapping needs to be manually adjusted. This method is suitable for personal or home entertainment scenarios, requires integration with a reverse proxy for public network access, and involves the complex configuration logic of the RetroArch emulator, with attention needed for compatibility issues between default hotkeys and actual operations.
Qwen3-14B · 2026-06-18

Preface

Today, having some free time, I suddenly thought of building a web-based retro game emulator. So I did a search on hub.docker and found the project linuxserver/emulatorjs. The official explanation is: "Browser-based web emulation, portable to almost any device, used for many retro game consoles". In fact, this is just using a web-based approach to implement the RetroArch emulator.

Its supported architectures are:

image.png

My primary Docker environment is an M1 Mac Mini (arm64), and my backup Docker environment is an Intel 13th Gen i5-based mini PC (x86-64). Both work fine, so this time I will deploy it on the backup Docker environment.

Deploy emulatorjs

Create the directories to be used in advance

Create configuration file directory:

mkdir -p /docker/emulatorjs/config

Create game ROM directory:

mkdir -p /docker/emulatorjs/data

Since I have existing NES and GBA ROMs, I created two additional folders and copied the corresponding ROMs into them:

mkdir -p /docker/emulatorjs/data/nes/roms
mkdir -p /docker/emulatorjs/data/gba/roms

Important:
emulatorjs supports emulators for various game consoles, with the types as follows:

  • 3do
  • arcade
  • atari2600
  • atari7800
  • colecovision
  • doom
  • gb
  • gba
  • gbc
  • jaguar
  • lynx
  • msx
  • n64
  • nds
  • nes
  • ngp
  • odyssey2
  • pce
  • psx
  • sega32x
  • segaCD
  • segaGG
  • segaMD
  • segaMS
  • segaSaturn
  • segaSG
  • snes
  • vb
  • vectrex
  • ws

The ROMs for these consoles are organized in the data folder under subfolders named after the ”console type”/roms. For example, the storage path for NES ROMs is:/data/nes/roms, and the storage path for GBA ROMs is:/data/gba/romsTherefore, if you already have the ROMs for the corresponding consoles, you can create the corresponding folders in advance, copy the ROMs into them, and then bind them using the -v option when creating the Docker container. This saves you the trouble of uploading the ROMs separately later.


Deploy emulatorjs

The command is as follows:

docker run --name=emulatorjs -d --restart=always --network=public-net \
  -e PUID=0 \ #可选功能,PUID和PGID的具体数值由id命令决定,比如我的root账号,用"id root"输出为"uid=0(root) gid=0(root) groups=0(root)",如果遇到权限问题可以尝试该参数
  -e PGID=0 \ #同上
  -e TZ=Asia/Shanghai \ #指定时区
  -e SUBFOLDER=/  \ #可选功能,如果反向代理使用了代理目录功能,则需该参数配合使用(例如该容器部署在云主机上,需要通过一个域名同时访问管理控制台和模拟器前端这种场景)。
  -p 3500:3000 \ #将宿主机3500端口映射到容器3000端口,这是管理控制台端口,大家根据实际情况修改
  -p 9600:80 \  #将宿主机9600端口映射到容器80端口,这是模拟器前端访问端口,用于浏览和启动游戏,大家根据实际情况修改
  -p 4001:4001  \ #可选功能,IPFS对等端口,如果想参与P2P网络分发前端作品,请在出口设备上做好此端口的映射
  -v /docker/emulatorjs/config:/config \ #配置文件挂载目录
  -v /docker/emulatorjs/data:/data \ # 游戏数据文件挂载目录
  -v /docker/emulatorjs/data/nes/roms:/data/nes/roms \ #可选操作,指定nes游戏rom目录
  -v /docker/emulatorjs/data/gba/roms:/data/gba/roms \ #可选操作,指定gba游戏rom
  linuxserver/emulatorjs:latest

When using the above commands, please delete the comment content after # yourself.

Access emulatorjs via the management console for administration

Initialize files

Access emulatorjs using http://[Host_IP]:3500, and the following interface will appear:

image.png

Click "download" in the red box in the image above, and the initialization file download will start automatically:
image.png

When "Downloaded All Files" is displayed in the red box at the top left of the image above, it means the download is complete. At this point, click the black square button in the red box at the top right to close the interface.

ROM Management

The image below shows the content of Rom Management:

image.png

In the image above, you can see that the ROMs in the GBA and NES folders I mapped in advance have actually been recognized, but you still need to click "scan" to scan them:
image.png

Click "gba" in the red box in the image below to enter:
image.png

Then click "Add All Roms to Config" in the red box in the image below to add the scanned games on the right to the configuration:
image.png

Do the same for NES.

If you update the contents of the corresponding ROMs folder in the future, you will need to use "Scan" here to rescan. The default "DL/Update" is to automatically update other system files again, which are the files downloaded during the previous initialization.

Config Management

The image below shows the content of Config Management:

image.png

Here are the key parameters for modifying each type of emulation. Just take a look, don't change anything.

File Management

The image below shows the content of File Management:

image.png

Here you can directly manage the contents of the data folder mounted by the container using the -v parameter. Subsequent ROM uploads are done here (of course, you can also operate directly on the host machine), such as uploading GBA ROMs:

image.png

If you didn't use the -v parameter to directly specify the directory containing the specific game ROMs like I did earlier, you will need to enter the ROM folder of the corresponding game type here (such as /gba/roms/ in the image above), upload the ROMs using "upload", and then return to the Rom Management interface to re-run "scan" for the corresponding game type:

image.png

Profile Management

The image below shows the content of Profile Management:

image.png

This is used to create usernames, passwords, and configuration files (retroarch.cfg) for different accounts. When accessing the emulator frontend, you can log in with the same account to upload and download configurations. This is very useful. Simply put, you log in to an account using one browser, complete the key configuration, and then upload the configuration. Then, log in to the same account in another browser and download the configuration, thereby achieving key configuration synchronization. To directly modify the default configuration, you need to use the default account. For details, please refer to the article:Modifying the default key configuration in the emulatorjs emulator


In addition, I also tried to directly modify the default settings in the source code, hoping to change the default configuration once and for all, but I didn't succeed. You can also try it. The specific path inside the container is as follows:
/emulatorjs/frontend/data/emu-main.js, you can run the following command on the host machine to open it directly with vi inside the container:

docker exec -it emulatorjs vi /emulatorjs/frontend/data/emu-main.js

Directly use /defaultControllers in vi and then press Enter to search for the keyword ”defaultControllers”:

image.png

In the image above, the value after "value" is the keyboard key, and the value after "value2" is the gamepad. The key definitions for the numbers in front are as follows:
image.png

Interested friends can look into this. After I modified emu-main.js, it did not take effect. The only post online mentioning this had only one sentence, saying that the loader.js file also needs to be modified to make emulator.js load before emu-main.js. Since I don't know programming, I studied loader.js for a long time but still couldn't understand it (I even specifically looked at a Java beginner tutorial...). In the end, I gave up. I hope friends who understand programming can complete this final step.

Note: emu-main.js, emulator.js, and loader.js are all in the container's/emulatorjs/frontend/data/directory.


Access emulatorjs via the emulator frontend to play games

Use http://:9600 to access the frontend interface:

image.png

Click the red box in the upper left corner to enter the username and password login interface:
image.png

The username and password in the red box in the image above are the ones we created earlier in the Profile Management section. You can also choose not to log in, which will correspond to the default user.

Since I only have GBA and NES games, there are only 2 options on the interface above. You can use the up and down arrow keys on the keyboard or the mouse wheel to select the type, and then press Enter to enter and see the specific games:

image.png

Similarly, use the mouse wheel or the up and down arrows on the keyboard to select the corresponding game, and then press Enter to enter:
image.png

For games, the default keyboard keys are: Up, Down, Left, and Right correspond to the 4 small arrows, Z and X are B and A, Right Shift is Select, and Enter is Confirm. (I just want to ask, what was the genius who set these default keyboard keys thinking...?)

If you want to modify the keys, after entering a specific game, press F1, and then modify them in the following order:

image.png

image.png

image.png

Do not directly use "Controls" in the "Quick Menu" brought up by F1, as shown below:

image.png

You cannot change keys here; you can only select existing mappings (at least that's the case for me).

The final effect is as follows:

image.png

Configure public network access

If you want to publish to the public network, you need to choose the most suitable publishing method based on your actual environment and the reverse proxy you use. You can refer to several of my previous articles:
1、Docker Series: Building Your Own Reverse Proxy Based on NPM Using Docker
2、Linux Panel Series: Configuring Reverse Proxy and Publishing Using Non-443 Ports
3、Home Data Center Series: Getting Cloudflare for Free via Domestic ICP-Filed Cloud Hosts to Achieve Fast Access to Domestic Sites from Abroad
4、Home Data Center Series: Getting Cloudflare for Free via Home Broadband Without Public IP to Achieve Fast Website Building (Universal)

Among them, the 1st and 2nd methods are suitable for environments with a public IP but without a legal 443 port (home broadband, non-filed cloud hosts), where non-standard ports need to be added after the URL (if using Cloudflare to build a website, no port needs to be added, but you need to customize the origin server port, which you can refer to:Home Data Center Series: Solving the Problem of Having a Public IP but No Legal 80 or 443 Ports for Website Building via Cloudflare's Origin Rules). The 3rd method is suitable for cloud hosts with ICP filing, and the 4th method is suitable for all environments (including environments without a public IP), which is also my recommended method (regardless of whether your environment has a public IP, because this method does not require running HTTPS traffic directly on the public network).

There is also another issue involved here, which is that there are 2 ports: one is the management console port, and the other is the frontend game port. I suggest only publishing the frontend game port. If you want to publish both ports but only use one domain name, you will need to use the proxy directory function of a reverse proxy, and at the same time, coordinate with the-e SUBFOLDERparameter.

Afterword

Finally, I should mention that the subsequent configuration actually enters another territory: the configuration of the RetroArch emulator. To be honest, I'm still completely confused by it. The configuration logic of this emulator is so counter-intuitive. Moreover, because many keys on the keyboard have functions assigned by default but aren't shown in the emulator's configuration options, it leads to conflicts after you finish configuring. For example, by default, both 'k' and 'p' are pause. If you map other functions to 'k' and 'p', pressing them will also pause the game at the same time... I'll see if I'm in the mood to write another article about this after I've used it for a while.

Note: After playing for a bit, here are the default hotkeys I've found so far:
H: reset
K and P: pause
L and Space: Fast forward (Hold L to fast forward, release to stop; press Space once to fast forward continuously, press again to stop. Don't just press the Space key randomly... My habit developed in World of Warcraft is very hard to adapt to here)
n and m: shader: ”retroarch.glslp” (don't know what it's for)
Numpad + and -: Adjust volume

📌 Content Structure Prompt:
This content belongs to the "Blog Knowledge Map" part, you can view the complete content path from here: Blog Knowledge Map 。
View Related Categories · 3 Matches
📎 Related Articles
Share this article
The blog content is original, please indicate the source when reposting! The RSS address of the blog is:https://blog.tangwudi.com/feed, welcome to subscribe; if needed, you can join theTelegram Groupto discuss questions together.
No Comments

Send Comment Edit Comment


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗ
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
Kaomoji
Emoji
Little Dinosaur
Flower!
Previous
Next
       

👋 Welcome to “Wudi's Personal Blog”

Here, long-term exploration is mainly carried out around the following directions:

🧱 Personal Digital Infrastructure and Blog System Construction
☁️ Cloudflare and Network Architecture Practice
🧠 AI and Knowledge System Exploration
🛡️ Network Security and Access Optimization
🎵 Music and Sound Cognition
👁️ Cognitive Perspectives and Worldviews