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:

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:

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

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:

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:

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

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:

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:

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:

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:

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:

Profile Management
The image below shows the content of Profile Management:

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”:

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:

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:

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

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:

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:

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:



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

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:

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