References:
https://learn.microsoft.com/en-us/windows/wsl/install
https://docs.docker.com/desktop/features/wsl/
https://container-registry.oracle.com (database/express)
https://www.oracle.com/tools/downloads/sqldev-downloads.html
WSL 2 is a full Linux kernel built by Microsoft, allowing Linux containers to run natively without emulation.
Requirements: Windows 11
To check your system type run systeminfo | find "System Type".
To check your Windows version press Windows logo key + R, type winver, select OK.
Open PowerShell as Administrator and run:
wsl --install
This enables all required features, installs the WSL 2 kernel, sets version 2 as default, and installs Ubuntu.
Restart when it finishes, then create your Linux user account and password.
Confirm that your distro runs version 2: Docker Desktop integration only works with WSL 2 distros:
wsl -l -v
NAME STATE VERSION * Ubuntu Running 2
To limit the memory and CPU that WSL 2 can use, create the file C:\Users\<yourUserName>\.wslconfig.
By default WSL 2 can use up to 50% of the host RAM. Oracle XE needs at least 2 GB for itself, so do not set the limit below 4 GB when running the database:
[wsl2] memory=4GB # Limits VM memory in WSL 2 processors=2 # Makes the WSL 2 VM use two virtual processors
Run wsl --shutdown to apply the settings.
See https://learn.microsoft.com/en-us/windows/wsl/wsl-config#configure-global-options-with-wslconfig for all options.
These port-forwarding steps are only needed if you want to reach your WSL 2 distro from another machine.
(e.g., expose the Oracle listener port 1521).
References:
https://learn.microsoft.com/en-us/windows-server/networking/technologies/netsh/netsh-interface-portproxy
https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/netsh-advfirewall-firewall-control-firewall-behavior.
$ sudo apt update $ sudo apt install openssh-server $ sudo apt install net-tools
/etc/ssh/sshd_config (sudo vi /etc/ssh/sshd_config).
Set the listen address to "0.0.0.0" and, optionally, change the SSH port (make sure the port is not already used).
If you connect with a password, also verify PasswordAuthentication yes:
Port 1521 ListenAddress 0.0.0.0Then restart and check the OpenSSH server:
$ sudo systemctl daemon-reload $ sudo service ssh restart $ sudo systemctl status ssh
C:\Users\mtitek>netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=1521 connectaddress=172.26.110.211 connectport=1521
C:\Users\mtitek>wsl hostname -I 172.26.110.211Important: the WSL 2 IP address might change after a restart, so this portproxy rule must be updated (delete/add) whenever the address changes. You can avoid portproxy entirely with
networkingMode=mirrored in .wslconfig, which makes WSL share the host network.C:\Users\mtitek>netsh interface portproxy show v4tov4
Listen on ipv4: Connect to ipv4: Address Port Address Port ------- ---- ------- ---- 0.0.0.0 1521 172.26.110.211 1521
C:\Users\mtitek>netsh advfirewall firewall add rule name="Open Port 1521 - WSL 2" dir=in action=allow protocol=TCP localport=1521To check the rule (
select-string is a PowerShell cmdlet):
PS C:\Users\mtitek> netsh advfirewall firewall show rule status=enabled dir=in name=all | select-string -pattern "(LocalPort.*1521)" -context 9,4
Rule Name: Open Port 1521 - WSL 2 ------------------------------------------- Enabled: Yes LocalPort: 1521 ...
To remove the port-forwarding and firewall rule configurations:
C:\Users\mtitek>netsh advfirewall firewall delete rule name="Open Port 1521 - WSL 2" C:\Users\mtitek>netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=1521
$ docker version
Client: Version: 29.5.2 API version: 1.54 ... Server: Docker Desktop Engine: Version: 29.5.2 ...
The official Oracle XE image is hosted on Oracle's container registry. Sign in once with your Oracle account (accept the database/express license terms on container-registry.oracle.com first), then pull and run:
$ docker login container-registry.oracle.com
$ docker pull container-registry.oracle.com/database/express:21.3.0-xe
$ docker run -d --name oracle-xe \
-p 1521:1521 \
-p 5500:5500 \
-e ORACLE_PWD=SYSPWD \
-v oracle-data:/opt/oracle/oradata \
container-registry.oracle.com/database/express:21.3.0-xe
-p 1521:1521: the database listener port.-p 5500:5500: the Enterprise Manager Express HTTPS port.ORACLE_PWD: sets the password for the SYS, SYSTEM, and PDBADMIN accounts.-v oracle-data:/opt/oracle/oradata: a named volume so the database files survive container removal.$ docker logs -f oracle-xe
Test-NetConnection -ComputerName localhost -Port 1521
ComputerName : localhost RemoteAddress : ::1 RemotePort : 1521 InterfaceAlias : Loopback Pseudo-Interface 1 SourceAddress : ::1 TcpTestSucceeded : TrueLook for TcpTestSucceeded : True. This confirms something is listening on 1521 on Windows localhost; it doesn't tell you, though, that it's actually Oracle, just that the port is open and accepting TCP connections.
netstat -ano | findstr 1521
TCP 0.0.0.0:1521 0.0.0.0:0 LISTENING 5368 TCP 0.0.0.0:1521 0.0.0.0:0 LISTENING 11304 ...This shows the PID bound to port 1521 on Windows. Since Docker Desktop on Windows publishes container ports directly to the Windows network stack (independent of the WSL/portproxy path discussed earlier), you should see it listed here once the container is running.
nc -zv localhost 1521
Connection to localhost (127.0.0.1) 1521 port [tcp/*] succeeded!
curl -v telnet://localhost:1521
* Host localhost:1521 was resolved. * IPv6: ::1 * IPv4: 127.0.0.1 * Trying [::1]:1521... * Connected to localhost (::1) port 1521
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ad287a31a02e container-registry.oracle.com/database/express:21.3.0-xe "/bin/bash -c $ORACL…" 58 minutes ago Up 58 minutes (healthy) 0.0.0.0:1521->1521/tcp, [::]:1521->1521/tcp, 0.0.0.0:5500->5500/tcp, [::]:5500->5500/tcp oracle-xe
$ docker exec -ti oracle-xe bash
bash-4.2$ sqlplus sys/SYSPWD@localhost:1521/XE as sysdba
SQL*Plus: Release 21.0.0.0.0 - Production on Sun Jun 21 15:17:39 2026
Version 21.3.0.0.0
Copyright (c) 1982, 2021, Oracle. All rights reserved.
Connected to:
Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0
SQL> SELECT USER || ' - ' || SYS_CONTEXT('USERENV', 'CON_NAME') FROM DUAL;
USER||'-'||SYS_CONTEXT('USERENV','CON_NAME')
--------------------------------------------------------------------------------
SYS - CDB$ROOT
The container exposes two services: XE (the CDB root, for admin connections)
and XEPDB1 (the default pluggable database, for application connections).
Download SQL Developer from
https://www.oracle.com/tools/downloads/sqldev-downloads.html.
Download the "Windows 64-bit with JDK included" version, unzip it, and run sqldeveloper.exe.
Create the admin connection in SQL Developer (CDB root): Username: sys, Password: SYSPWD, Hostname: localhost, Port: 1521, Service name: XE, Role: SYSDBA.
Connect as sys and switch to the pluggable database XEPDB1.
application schemas must live in the PDB, never in the CDB root. The tablespace must exist before it can be referenced by CREATE USER:
ALTER SESSION SET CONTAINER = XEPDB1; CREATE TABLESPACE MTITEK_DATA_TS DATAFILE '/opt/oracle/oradata/XE/XEPDB1/mtitek_data_ts_1.dbf' SIZE 500M AUTOEXTEND ON; CREATE USER MTITEK_APP IDENTIFIED BY MTITEKPWD DEFAULT TABLESPACE MTITEK_DATA_TS QUOTA UNLIMITED ON MTITEK_DATA_TS; GRANT CONNECT, RESOURCE TO MTITEK_APP;
MTITEK_APP is the schema/user.
MTITEK_DATA_TS is the tablespace that physically stores its data.
Create the application connection in SQL Developer: Username: MTITEK_APP, Password: MTITEKPWD, Hostname: localhost, Port: 1521, Service name: XEPDB1, Role: default.
Verify the schema with a first table:
CREATE TABLE MTITEK_TABLE (
ID NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
LABEL VARCHAR2(100) NOT NULL
);
INSERT INTO MTITEK_TABLE (LABEL) VALUES ('Hello Oracle!');
SELECT * FROM MTITEK_TABLE;