The fastest way to get a real Oracle Database on your laptop is a container. No installer, no
listener configuration, no ORACLE_HOME, and you can throw it away when you are done.
Oracle Database Free (23ai) is the edition to use. It is free for development and production, capped at 2 CPUs, 2 GB of SGA and 12 GB of user data — far more than a tutorial needs.
The image
Oracle publishes the official image on its own registry, not Docker Hub:
docker pull container-registry.oracle.com/database/free:latestNo login is required for the Free image. It is large — roughly 9 GB — so start the pull and go get
a coffee. There is a :latest-lite tag that drops the non-English locales and some
components and is about half the size.
There is also a well-maintained community image, gvenzl/oracle-free, which is
smaller, starts faster and adds convenience environment variables. It is the same database
underneath. Use whichever you prefer; the commands below work with both.
Run it
docker run -d \
--name oracle-free \
-p 1521:1521 \
-e ORACLE_PWD=Welcome1 \
-e ORACLE_CHARACTERSET=AL32UTF8 \
-v oracle-free-data:/opt/oracle/oradata \
container-registry.oracle.com/database/free:latestThree of those flags matter more than they look:
ORACLE_PWDsets the password forSYS,SYSTEMandPDBADMIN. Omit it and the image generates a random one that you then have to dig out of the logs.ORACLE_CHARACTERSET=AL32UTF8is Oracle's UTF-8. The character set is fixed at database creation and changing it afterwards is a migration, not a setting.- The named volume is what makes the database survive
docker rm. Without it, first boot runs the ~10-minute database creation again every time.
Wait for it to actually be ready
First boot creates the database, which takes several minutes. The container is "up" long before the database accepts connections, so watch for the banner rather than guessing:
docker logs -f oracle-free
# ...
# #####################################
# DATABASE IS READY TO USE!
# #####################################In a script, poll the healthcheck instead:
until [ "$(docker inspect -f '{{.State.Health.Status}}' oracle-free)" = "healthy" ]; do
sleep 5
done
echo "ready"Connect: the bit everyone gets wrong
The container runs a CDB called FREE containing a
PDB called FREEPDB1. Your tables belong in the PDB. Connect to the CDB
root by mistake and every CREATE USER fails with
ORA-65096: invalid common user or role name.
# Right: the PDB, via the Easy Connect service-name form
sqlplus system/Welcome1@//localhost:1521/FREEPDB1
# Wrong for app work: this lands you in the CDB root
sqlplus system/Welcome1@//localhost:1521/FREEIf you do not have a local client, use the one inside the container:
docker exec -it oracle-free sqlplus system/Welcome1@//localhost:1521/FREEPDB1Confirm where you landed before doing anything else:
SELECT sys_context('USERENV', 'CON_NAME') AS container,
sys_context('USERENV', 'CURRENT_USER') AS username
FROM dual;
-- CONTAINER USERNAME
-- ----------- ---------
-- FREEPDB1 SYSTEMUse SQLcl, not SQL*Plus
sqlplus is a 1980s teletype interface with no history, no editing and no completion.
SQLcl is Oracle's modern replacement — a single Java download, same command syntax,
plus arrow-key history, tab completion, and output formats that are actually readable:
brew install --cask sqlcl # or download the zip from Oracle
sql system/Welcome1@//localhost:1521/FREEPDB1SET SQLFORMAT ansiconsole -- columns sized to the data, no more COLUMN commands
SET SERVEROUTPUT ON -- so DBMS_OUTPUT.PUT_LINE is visible
INFO orders -- columns, indexes and constraints in one shot
DDL orders -- reconstruct the CREATE TABLE statementThose last two commands alone justify the download.
docker-compose
Worth committing to the repo so the whole team gets the same database:
services:
oracle:
image: container-registry.oracle.com/database/free:latest
container_name: oracle-free
ports:
- "1521:1521"
environment:
ORACLE_PWD: Welcome1
ORACLE_CHARACTERSET: AL32UTF8
volumes:
- oracle-data:/opt/oracle/oradata
# Every .sql/.sh in here runs once, after the database is created.
- ./db/init:/opt/oracle/scripts/startup
healthcheck:
test: ["CMD", "/opt/oracle/checkDBStatus.sh"]
interval: 20s
timeout: 10s
retries: 30
start_period: 10m
volumes:
oracle-data:The startup mount is the useful part: drop a 01-schema.sql and a
02-seed.sql in ./db/init and a fresh volume comes up with your schema
already in it.
Gotchas
- Apple Silicon. Oracle publishes an arm64 variant of the Free image, so a
plain
docker pullusually just works. If you land on an amd64-only manifest, add--platform linux/amd64and accept the emulation penalty — startup goes from about three minutes to ten, but it runs. - Memory. Give Docker at least 4 GB. Less and database creation dies partway through, leaving a volume you have to delete before retrying.
- Port 1521 already in use means an old container is still around. Map to
-p 1522:1521and adjust the connect string. ORA-12514— "listener does not currently know of service" — is almost always a typo in the service name, or connecting before the database finished creating.- Re-running init scripts. They only execute when the database is created. If
you edited them,
docker rm -f oracle-free && docker volume rm oracle-dataand start over.
Next
You are connected as SYSTEM, which you should never build an application on. Next:
users, schemas and the privileges an application account actually needs.