Oracle Database – Run It Locally with Docker

January 15, 20244 min readUpdated 8/4/2026

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

No 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:latest

Three of those flags matter more than they look:

  • ORACLE_PWD sets the password for SYS, SYSTEM and PDBADMIN. Omit it and the image generates a random one that you then have to dig out of the logs.
  • ORACLE_CHARACTERSET=AL32UTF8 is 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/FREE

If you do not have a local client, use the one inside the container:

docker exec -it oracle-free sqlplus system/Welcome1@//localhost:1521/FREEPDB1

Confirm 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    SYSTEM

Use 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/FREEPDB1
SET 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 statement

Those 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 pull usually just works. If you land on an amd64-only manifest, add --platform linux/amd64 and 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:1521 and 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-data and 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.