New JS API in APEX 21.2

As John already teased in his blog post about the JavaScript API changes in APEX 21.2, there is a new namespace coming.

Main purpose of it is to have an out-of-the-box replacement for Moment.js which is part of APEX since we introduced our Calendar region with FullCalendar version 3. In APEX 21.1 this region was updated to use the newer version 5 of the underlying FullCalendar library, and this particular version doesn’t come with Moment.js anymore and changed many other things, especially from an API point of view. If you’re interested in these changes in more detail, we created a blog posting back when 21.1 was released to help developers to get started with this new version.

Additionally even Moment.js itself states, that you shouldn’t use this library anymore:

So we thought it would be really helpful for all APEX developers to have an integrated API which can do most of the things Moment.js did + things that make sense in an APEX or database environment.

There are 4 main categories in which you will find functions to use in

  • Date Manipulation
  • Date Formatting & Parsing
  • Date Comparison
  • Miscellaneous Functions

Before starting with the description of the actual categories and showing some code examples, it is worth to mention that operates with native JavaScript date objects and doesn’t introduce new custom objects like Moment.js did.

var date = new Date();

Date Manipulation

This category contains functions to manipulate JavaScript date objects, like adding or subtracting given amounts to or from a date.

So lets add something to a given date:

var date = new Date();

// add 2 years date, 2, );

// add 5 days date, 5, );

// add 45 minutes date, 45, );

console.log( date );

And now lets subtract something from a date:

var date = new Date();

// subtract 2 months date, 2, );

// subtract 30 seconds date, 30, );

// subtract 4 weeks date, 4, );

console.log( date );

As you can see, we used the object in both examples, this has constants for all units used by

Date Formatting & Parsing

In this category there’s everything related to date formatting & parsing. The most important part to mention is that handles it formatting & parsing logic using Oracle Database compatible format masks, which makes it easy for APEX developers to re-use their DB logic also on the client, without investigating too much brain power in thinking in 2 separate format mask flavors.

Date formatting:

var date = new Date(),
    dateString = "";

dateString = date, "YYYY-MM-DD HH24:MI:SS" );
// output: 2021-10-11 22:15:45

dateString = date, "YYYY/MM/DD" );
// output: 2021/10/11

dateString = date, "YYYY, IW" );
// output: 2021, 41

dateString = date, "Day, DD Month YYYY" );
// output: Monday, 11 October 2021

dateString = date, "DL" );
// output: Monday, October 11, 2021

dateString = date, "DL", "de" );
// output: Montag, 11. Oktober 2021

dateString = date, "DL", "es" );
// output: lunes, 11 de octubre de 2021

// relative date / time as words
dateString = date );
// output: "10 months from now" or "2 months ago"

dateString = date, true );
// output: "in 10mo" or "2mo"

Date parsing:

var date;

date = "2021-10-11 22:15", "YYYY-MM-DD HH24:MI");

date = "2021/10/11", "YYYY/MM/DD");

date = "2021-OCT-11 22:15:45", "YYYY-MON-DD HH24:MI:SS");

If no format or language is specified, automatically uses the format and language defined in your application. You can also get this information using:


Date Comparison

This category contains functions useful to compare 2 or more JavaScript date objects with each other.

var date1 = "2021/10/01", "YYYY/MM/DD"),
    date2 = "2021/10/15", "YYYY/MM/DD"),
    date3 = "2021/10/31", "YYYY/MM/DD");

// is date 1 < date 2 date1, date2, );
// output: true

// is date 1 > date 2 date1, date2, );
// output: false

// is date 1 = date 2 date1, date2, );
// output: false

// is date 1 <= date 2 date1, date2, );
// output: true

// is date 1 >= date 2 date1, date2, );
// output: false

// is date 1 between date 2 and date 3 date1, date2, date3, );
// output: false

// get the minimum date of given date arguments
var minDate = date1, date2, date3 );
// output: date1 as date object

// get the maximum date of given date arguments
var maxDate = date1, date2, date3 );
// output: date3 as date object

Most compare functions are also utilizing constants. This enables developers to precisely compare dates from milliseconds up to years.


Beside the other categorized functionality, also has more generic functions or useful helper functions on board, which I won’t describe here in full length, but to just get an idea, here are a few of them:

var date = new Date();

// get ISO week number
var week = date );

// get total days of month
var days = date );

// get first of month as date object
var first = date );

// get last of month as date object
var last = date );

// get months between 2 dates
var months = now, date );

// check if it's a leap year
var isLeapYear = date );

The whole API documentation is part of our official JavaScript API documentation, which when APEX 21.2 is ready will be available here. I also plan to publish a demo app show casing everything in this post live in an APEX app. This app will be released as soon as 21.2 is available on

Edit 18/11/2021: As promised, and now that APEX 21.2 pre-production release is available at, you can find the demo app here.

Happy APEXing! 🚀

Check local APEX plugin version against

There’s something new on A REST endpoint to retrieve information about listed open source plugins from the plugins directory!
Just fire up a HTTP GET call to

to retrieve all plugin information in JSON format…

You can now use this information to compare your locally installed plugins (which of course are installed from before) with the ones listed on In this example I use it to compare the locally installed plugin version to the newest version available, very useful for update checks where you want to know if there’s a newer version of the plugins available!

Just create a view like this in your schema (using APEX_DATA_PARSER: for all DBs and APEX versions >= 19.1):

create or replace force view v_apex_world_plugins as
select apex_world_plugins_t.line_number
      ,apex_world_plugins_t.col001      as plg_int_name
      ,apex_world_plugins_t.col002      as plg_name
      ,apex_world_plugins_t.col003      as plg_desc
      ,apex_world_plugins_t.col004      as plg_type
      ,apex_world_plugins_t.col005      as plg_version
      ,apex_world_plugins_t.col006      as created
      ,apex_world_plugins_t.col007      as updated
      ,apex_world_plugins_t.col008      as apex_versions
      ,apex_world_plugins_t.col009      as demo_url
      ,apex_world_plugins_t.col010      as download_url
      ,apex_world_plugins_t.col011      as homepage_url
      ,apex_world_plugins_t.col012      as like_count
      ,apex_world_plugins_t.col013      as author_name
      ,apex_world_plugins_t.col014      as author_email
      ,apex_world_plugins_t.col015      as author_url
      ,apex_world_plugins_t.col016      as author_twitter
  from table(apex_data_parser.parse(p_content      => apex_web_service.make_rest_request_b(p_url         => ''
                                                                                          ,p_http_method => 'GET')
                                    ,p_file_name    => 'apex_world_plugins.json'
                                    ,p_file_profile => q'[
  "file-type": 4,
  "single-row": false,
  "file-encoding": "AL32UTF8",
  "row-selector": "items",
  "headings-in-first-row": false,
  "csv-enclosed": "\"",
  "columns": [
      "name": "PLG_INT_NAME",
      "data-type": 1,
      "data-type-len": 255,
      "selector": "plg_int_name"
      "name": "PLG_NAME",
      "data-type": 1,
      "data-type-len": 255,
      "selector": "plg_name"
      "name": "PLG_DESC",
      "data-type": 1,
      "data-type-len": 4000,
      "selector": "plg_desc"
      "name": "PLG_TYPE",
      "data-type": 1,
      "data-type-len": 50,
      "selector": "plg_type"
      "name": "PLG_VERSION",
      "data-type": 1,
      "data-type-len": 50,
      "selector": "plg_version"
      "name": "CREATED",
      "data-type": 5,
      "selector": "created",
      "format-mask": "YYYY\"-\"MM\"-\"DD\"T\"HH24\":\"MI:SSTZR"
      "name": "UPDATED",
      "data-type": 5,
      "selector": "updated",
      "format-mask": "YYYY\"-\"MM\"-\"DD\"T\"HH24\":\"MI:SSTZR"
      "name": "APEX_VERSIONS",
      "data-type": 1,
      "data-type-len": 255,
      "selector": "apex_versions"
      "name": "DEMO_URL",
      "data-type": 1,
      "data-type-len": 255,
      "selector": "demo_url"
      "name": "DOWNLOAD_URL",
      "data-type": 1,
      "data-type-len": 255,
      "selector": "download_url"
      "name": "HOMEPAGE_URL",
      "data-type": 1,
      "data-type-len": 255,
      "selector": "homepage_url"
      "name": "LIKE_COUNT",
      "data-type": 2,
      "decimal-char": ".",
      "selector": "like_count"
      "name": "AUTHOR_NAME",
      "data-type": 1,
      "data-type-len": 100,
      "selector": "author_name"
      "name": "AUTHOR_EMAIL",
      "data-type": 1,
      "data-type-len": 255,
      "selector": "author_email"
      "name": "AUTHOR_URL",
      "data-type": 1,
      "data-type-len": 255,
      "selector": "author_url"
      "name": "AUTHOR_TWITTER",
      "data-type": 1,
      "data-type-len": 50,
      "selector": "author_twitter"
  "parsed-rows": 200
]')) apex_world_plugins_t;

Or like this (using XMLTABLE: for all DBs and APEX versions):

create or replace view v_apex_world_plugins as
select apex_world_plugins_t.plg_int_name
  from xmltable('/json/items/row' passing
                apex_json.to_xmltype(apex_web_service.make_rest_request(p_url         => ''
                                                                       ,p_http_method => 'GET')) columns plg_int_name path
               ,plg_name path 'plg_name'
               ,plg_desc path 'plg_desc'
               ,plg_type path 'plg_type'
               ,plg_version path 'plg_version'
               ,created path 'created'
               ,updated path 'updated'
               ,apex_versions path 'apex_versions'
               ,demo_url path 'demo_url'
               ,download_url path 'download_url'
               ,homepage_url path 'homepage_url'
               ,like_count path 'like_count'
               ,author_name path 'author_name'
               ,author_email path 'author_email'
               ,author_url path 'author_url'
               ,author_twitter path 'author_twitter') apex_world_plugins_t

Or like this (using JSON_TABLE: for DBs >= 12.1 and all APEX versions):

create or replace view v_apex_world_plugins as
select apex_world_plugins_t.plg_int_name
  from json_table(apex_web_service.make_rest_request(p_url         => ''
                                                    ,p_http_method => 'GET')
                 ,'$.items[*]' columns(plg_int_name varchar2(250) path '$.plg_int_name'
                         ,plg_name varchar2(250) path '$.plg_name'
                         ,plg_desc varchar2(4000) path '$.plg_desc'
                         ,plg_type varchar2(100) path '$.plg_type'
                         ,plg_version varchar2(50) path '$.plg_version'
                         ,created varchar2(50) path '$.created'
                         ,updated varchar2(50) path '$.updated'
                         ,apex_versions varchar2(100) path '$.apex_versions'
                         ,demo_url varchar2(1000) path '$.demo_url'
                         ,download_url varchar2(1000) path '$.download_url'
                         ,homepage_url varchar2(1000) path '$.homepage_url'
                         ,like_count number path '$.like_count'
                         ,author_name varchar2(250) path '$.author_name'
                         ,author_email varchar2(250) path '$.author_email'
                         ,author_url varchar2(1000) path '$.author_url'
                         ,author_twitter varchar2(100) path '$.author_twitter')) apex_world_plugins_t;

Note: Thus the web service is running over HTTPS a Oracle Wallet is needed to connect to it!
I created a script which creates such a wallet with all valid public root CAs a while back:

And with this select statement you get the local plugin version and the version from for easier comparison:

      ,apex_appl_plugins.version_identifier as version_local
      ,v_apex_world_plugins.plg_version     as version_apex_world
  from apex_appl_plugins
 where = v_apex_world_plugins.plg_int_name
   and apex_appl_plugins.application_id = 103

The output should be like that:

And if it differs, it’s time for an update!

Just as easy as 123! 🙂

Oracle APEX Plugin Performance

Last week a customer of mine hired me to look into performance issues they had at larger forms within their APEX inhouse app.
After digging into it, I recognized that they used a lot of plugins in this particular app (~ 90), and of course a lot of known ones from the community like Select2 or Dropzone.

The problematic pages were mostly larger forms with a lot of items, in this case also a lot of plugin items, like mentioned above Select2. This item type plugin was used around 30 times on this page…

So first I had a look into the debug output of the page and I saw that most of the output was the PL/SQL source code of this plugin. So 30 times the APEX engine had to parse and execute the complete PL/SQL source of Select2, and the newest version of it has 773 lines of PL/SQL code…

The second problem I found was that all static files of the plugin (e.g JavaScript and CSS files) came from the database, you will see that in the debug output in APEX > 5.0 environments on the file path of the files that are loaded, e.g:

Load JavaScript file=demo/r/140/files/plugin/53116667758949585/v1/select2-apex.js

File paths which contain the virtual directory “/r/” shows us that the file being loaded comes from the DB, so each time a static file was loaded it hits the DB which returns a BLOB. Of course browsers will cache such a file after getting it once, but in my opinion the best place for static files is the web server itself not the DB…

So I found 2 problems in this case:
1) Large PL/SQL code of a plugin
2) Plugin static files are loaded from DB

A possible solution for both:
1) Transfer the PL/SQL code from the plugin to a real DB object, e.g a PL/SQL package
2) Transfer all plugin static files to the web server

After creating a PL/SQL package (e.g. select2_plg_pkg) with the copied source code from the plugin and moving all static files to the web server, the plugin looked like this:

So lets compare the page performance before and after the changes (30 Select2 items / 2 Dropzone Regions).
With the orginal plugins the page rendering took: ~ 4.3 seconds
With the modified plugins the page rendering took: ~ 1.3 seconds

This is an performance improvement of ~330% !! So the page is 3x faster in rendering than before. To be honest I didn´t expect such an performance increase by changing the plugin code…

If you want to see it in action, here´s a little demo app:
Use demo / demo for login!

As best practise learns us to have as less code in APEX as possible (e.g only function calls), this is also valid for plugins. So be careful in using plugins and always try to review the code. In this case the plugins are not bad coded or something like that, instead it´s the most easy way to share plugins with others, having files and PL/SQL source code inside the plugin. This means that you only need the plugin export SQL file to import and use it.

But some plugins have a lot of source code (here over 700 lines) which makes it problemtatic using such a plugin many times on a page, because this source code have to be parsed again and again.

If you decide to also improve the performance of your plugins like I told you here, be careful, thus the plugin now consists of several parts:
– the plugin export SQL file
– the static files / folder
– the pl/sql package
And if you want to share your plugin or use it in another application you have to import all 3 parts of it!

Another disadvantage would be, if an plugin developer updates the open source plugin you have to catch up the changes by yourself, because you have changed some logic inside of the plugin before…

Blockchain with Oracle and APEX

Thus the whole IT world talks about blockchains, the bitcoin price is as high as never (which underlying technology is a blockchain mechanism) and even Oracle has announced a new Blockchain Cloud Service at OOW, I looked into that topic and tried to create a simple blockchain by my own using the Oracle Database and of course APEX…

Before we can start developing on a blockchain mechanism we have to investigate some time to get a basic understanding what a blockchain is and how it works all together. So lets get started with some terminology:

What is a Blockchain?
A blockchain is a distributed database with a list (chain) of records (blocks) linked and secured by digital fingerprints (crypto hashes)

  @index = 0,
  @timestamp = 1637-09-15 20:52:38,
  @data = "Genesis",
  @previous_hash = "0",
  @hash = "edbd4e11e69bc399a9ccd8faaea44fb27410fe8e3023bb9462450a0a9c4caa1b">,
  @index = 1,
  @timestamp = 1637-09-15 21:02:38,
  @data = "Transaction Data...",
  @previous_hash = "edbd4e11e69bc399a9ccd8faaea44fb27410fe8e3023bb9462450a0a9c4caa1b",
  @hash = "eb8ecbf6d5870763ae246e37539d82e37052cb32f88bb8c59971f9978e437743">,
  @index = 2,
  @timestamp = 1637-09-15 21:12:38,
  @data = "Transaction Data......",
  @previous_hash = "eb8ecbf6d5870763ae246e37539d82e37052cb32f88bb8c59971f9978e437743",
  @hash = "be50017ee4bbcb33844b3dc2b7c4e476d46569b5df5762d14ceba9355f0a85f4">,

What’s a Hash? What’s a Crypto(graphic) Hash Digest Checksum?
A hash e.g. eb8ecbf6d5870763ae246e37539d82e37052cb32f88bb8c59971f9978e437743 is a small digest checksum calculated with a one-way crypto(graphic) hash digest checksum function e.g. SHA256 (Secure Hash Algorithm 256 Bits) from the data

sha =
sha.update( @index + @timestamp + @data + @previous_hash )

A blockchain uses

  • the block index (e.g. 1,2,3,4, etc.)

  • the block timestamp (e.g. 10-OKT-2017 16:16:59,216566 +02:00)

  • the block data (e.g. Transaction Data…)

  • the hash from the previous block (e.g. edbd4e11e69bc399a9ccd8faaea44fb27410fe8e3023bb9462450a0a9c4caa1b)

  • the hash (e.g. be50017ee4bbcb33844b3dc2b7c4e476d46569b5df5762d14ceba9355f0a85f4): calculated by the hash digest checksum of (@index + @timestamp + @data + @previous_hash)

By design, blockchains are inherently resistant to modification of the data. You can not modify one block without modifying all other blocks in the chain too.
If the blockchain is shared and distributed with others it is even more resistant to modification…

Now, after knowing some basic concepts about blockchains the next step would be: How can we integrate such technology into the Oracle Database? And of course a basic blockchain is kind of simple to implement.

Our blockchain would be nothing more than a simple table with the columns we already saw in the example above: index, timestamp, data, previous hash and hash.

One table row would then be one block of the blockchain.

So lets get finally into what we all like, PL/SQL stuff to explain it a bit better:

1. Our blockchain table

-- Create table
create table BLOCKCHAIN
  bc_index         number not null,
  bc_timestamp     timestamp with time zone not null,
  bc_data          clob not null,
  bc_previous_hash varchar2(500) not null,
  bc_hash          varchar2(500) not null
-- Create/Recreate primary, unique and foreign key constraints 
alter table BLOCKCHAIN
  add constraint BLOCKCHAIN_PK primary key (BC_INDEX);

2. A sequence that increments our index column (Primary Key)

-- Create sequence 
create sequence BLOCKCHAIN_SEQ
minvalue 1
maxvalue 9999999999999999999999999999
start with 1
increment by 1

3. An before insert trigger that set our index column to a new incremented value on each insert

-- Create Before Insert Trigger
  BEFORE INSERT ON blockchain
  IF :new.bc_index IS NULL THEN
    :new.bc_index := blockchain_seq.nextval;

After that, our blockchain is in place and waits to get filled with data. Before we can do this we will need some kind of API to do that.

So lets create a PL/SQL package that manages some basic functionality to interact with our blockchain.

That’s what it should be able to do in the end:

  • calculate hashes
  • get a block
  • add a block
  • check blockchain health (valid or not)
  • get complete blockchain (starting with specified index)

And guess what? The Oracle Database has all features to do this! Thus we need cryptographic functionality (hashing) we will use DBMS_CRYPTO for this, so we will need a grant to our schema:

-- Grant to DBMS_CRYPTO as SYS
GRANT EXECUTE ON sys.dbms_crypto TO blockchain_schema;

If you´re not able or allowed to grant this package, then there´s an alternative open source PL/SQL utility for that: OOS Utils

Now we can start with the PL/SQL package, here are only some code snippets to get a understanding, the whole source code with an APEX demo app is available on GitHub:

Get a block (basically get an table row record)

FUNCTION get_block(p_bc_index IN blockchain.bc_index%TYPE)
  RETURN blockchain%ROWTYPE IS
  l_blockchain_row blockchain%ROWTYPE;
    SELECT *
      INTO l_blockchain_row
      FROM blockchain
     WHERE blockchain.bc_index = p_bc_index;
    WHEN no_data_found THEN
      l_blockchain_row.bc_index         := 0;
      l_blockchain_row.bc_timestamp     := to_timestamp('01-01-1970 00:00:00',
                                                        'DD-MM-YYYY HH24:MI:SS');
      l_blockchain_row.bc_previous_hash := 0;
      l_blockchain_row.bc_hash          := 0;
  RETURN l_blockchain_row;
END get_block;

Calculate a SHA256 hash of a block

FUNCTION calculate_hash(p_bc_index       IN blockchain.bc_index%TYPE,
                        p_bc_timestamp   IN blockchain.bc_timestamp%TYPE,
                        p_bc_data        IN blockchain.bc_data%TYPE,
                        p_hash_algorithm IN PLS_INTEGER := dbms_crypto.hash_sh256)
  l_prev_blockchain_row blockchain%ROWTYPE;
  l_prev_hash           VARCHAR2(500);
  l_hash                VARCHAR2(500);
  l_hash_src            CLOB;
  l_prev_blockchain_row := blockchain_pkg.get_block(p_bc_index => blockchain_pkg.get_previous_block_index(p_current_index => p_bc_index));
  l_prev_hash           := l_prev_blockchain_row.bc_hash;
  l_hash_src := p_bc_index ||
                        'DD.MM.RRRR HH24:MI:SSXFF TZH:TZM',
                        'nls_numeric_characters = ''. ''') || p_bc_data ||
  l_hash := dbms_crypto.hash(l_hash_src,
  RETURN l_hash;
END calculate_hash;

Add a new block to our blockchain

FUNCTION add_block(p_bc_timestamp IN blockchain.bc_timestamp%TYPE := systimestamp,
                   p_bc_data      IN blockchain.bc_data%TYPE)
  RETURN blockchain.bc_index%TYPE IS
  l_prev_blockchain_row blockchain%ROWTYPE;
  l_prev_hash           VARCHAR2(500);
  l_hash                VARCHAR2(500);
  l_bc_index            blockchain.bc_index%TYPE;
  l_bc_index            := blockchain_seq.nextval;
  l_prev_blockchain_row := blockchain_pkg.get_latest_block;
  l_prev_hash           := l_prev_blockchain_row.bc_hash;
  l_hash                := blockchain_pkg.calculate_hash(p_bc_index     => l_bc_index,
                                                         p_bc_timestamp => p_bc_timestamp,
                                                         p_bc_data      => p_bc_data);
  INSERT INTO blockchain
  RETURN l_bc_index;
END add_block;

Check health of blockchain (valid or not)

FUNCTION is_blockchain_valid RETURN BOOLEAN IS
  l_current_block blockchain%ROWTYPE;
  l_prev_block    blockchain%ROWTYPE;
  l_current_hash  VARCHAR2(500);
  CURSOR l_cur_blockchain IS
    SELECT blockchain.bc_index
      FROM blockchain
     ORDER BY blockchain.bc_index;
  FOR l_rec_blockchain IN l_cur_blockchain LOOP
    l_current_block := blockchain_pkg.get_block(p_bc_index => l_rec_blockchain.bc_index);
    l_prev_block    := blockchain_pkg.get_block(p_bc_index => blockchain_pkg.get_previous_block_index(p_current_index => l_rec_blockchain.bc_index));
    l_current_hash  := blockchain_pkg.get_block_hash(p_blockchain_block => l_current_block);
    IF l_current_block.bc_hash != l_current_hash THEN
    END IF;
    IF l_current_block.bc_previous_hash != l_prev_block.bc_hash THEN
    END IF;
END is_blockchain_valid;

But nothings better than see it in action, here´s a APEX live demo:
As mentioned above, an export of this demo is available on GitHub

Hope this blog post is useful for some people, as I do think that this technology is very powerful and enables many use cases to save data in an secure and unmodifiable way, e.g transactional data or bookings etc…

Happy coding! 🙂

Awesome Oracle APEX List on GitHub

Maybe you have already seen my recent tweet about it:

So I´m calling now for community power to help extending that list with great articles, blogs, tips & resources.
Goal should be to make this awesome list even more awesome and a helpful guide / reference for everyone using Oracle APEX!

Here are some steps to contribute to this GitHub project:

  • Create a GitHub Account here

  • Fork the main GitHub repository. After that, you will find the forked repository under your GitHub user ( #github-user#/awesome-orclapex )

  • Edit the file and add a APEX blog, link, article or resource. Here´s a contribution guide. You can do it either in the GitHub web interface or via git command line tools

  • Create a Pull Request to pull your changes to the main repository. I will review the changes and merge it into the main Readme file.

That´s it!
Thanks for your contributions, together we can make this even more awesome! 🙂

Ps: Maybe you´re interested in the complete GitHub awesome stuff, then you will find many awesome projects here.

Deploy APEX Docker Image on Oracle Container Cloud Service

As I really like Docker and use it pretty much in my daily life as a developer, it became a inherent part in my workflow.
I use it for my development instances or for having test servers pretty fast available. Even sharing the images with other devs or have some demo environment within minutes, Docker sets the bar pretty high.

Usually I use my own made Docker Image which contains the following software:

  • Oracle Linux 7.3
  • Oracle Database Enterprise Edition with non-CDB architecture
  • Oracle APEX 5.1.2
  • Oracle ORDS 3.0.11
  • Oracle SQLcl
  • Apache Tomcat 8.0.45
  • Java JDK 8u144
  • OraOpenSource Logger 3.1.1
  • OraOpenSource OOS Utils 1.0.0
  • APEX Office Print 3.x (Cloud Package)

The build scripts are available on GitHub and open source: Oracle Database & APEX Developer Docker Image

With this software shipped in one Container I have everything in place to get my work done. (Of course I know that this is usually not the way Docker works, normally you have only one software component in one Docker container. But for me it works quite well, getting all the software I need without having the overhead of a real VM)

Oracle Cloud Services

So in the past I only used Docker on my local machines or servers but I was interested if my image works in the Oracle Cloud too. So I decided to give it a try, and lets have a look what Oracle have in petto.

(Anticipation: After getting used to the Oracle Cloud UI, which took only an hour or two, I really liked how the Database Cloud Service and Container Cloud Service worked)

First step was to get into the Oracle Cloud Services, and for that purpose Oracle offers a free try period, which enables you to test it either for 30 days or until you consumed services worth $300. Here you get started:

After my registration was successful, Oracle had my cloud account ready within 4 hours. (Which is OK, but of course it could be a bit faster if you have a look at other big cloud companies…). In the welcome mail, I got all the information I needed to get into the Oracle Cloud.

Thus we are only interested in the Container Cloud Service in this blog post, I will not write about the other possibilities within the Oracle Cloud. Having that said, I really liked the Database Service, too 🙂

Main focus is to get our own built Docker image running in the Oracle Container Cloud Service so we can use it and can connect to the services like APEX, Database or SSH.

Create an Oracle Container Cloud Service

This was really easy to accomplish, after login I navigated to the Container Service Section and created a new Service. The following screen shots will show the different steps and the create wizard.

About 30 minutes later I received an mail from Oracle, that my newly created Container Service is now up and running.

So I navigated to the detail page of my Container Service to get some information about it. The most important information is the Public IP address, this is the IP we use to connect to the admin panel of the Container Service:

https://<public-ip> e.g

Oracle Container Cloud Service
Having this information we can login to our Oracle Container Cloud Service entering https + Public IP into our browser. After we have successfully authenticated we will land on a dashboard page of the service.

For now we are finished within the Oracle Cloud web interface, we successfully got an cloud account, created a Container Service and logged into that service. This procedure was indeed straight forward and not that complicated. It took me about 5-10 minutes to get this done. Really nice!
Next is preparing our Docker image so we can use it in the Oracle Container Cloud Service.

Build Docker Image

I am not going into much detail here, because the whole procedure creating a Docker image is described on GitHub. Just to see the steps:

  • Clone or download the GitHub repository to your local machine
    git clone
  • Download missing software components (like Oracle DB or Oracle APEX …)
  • Build the Docker image
    docker build -t db-apex-dev-image .

Upload Docker image to Docker Hub

Thus it is needed that the image is uploaded to some location our Oracle Container Cloud Service has access to, I used Docker Hub to get this done. But of course you can use every other or maybe your own Docker registry server. Important is that the Oracle Cloud server can access your registry server.

(What is a Docker registry server? Basically it is a server which stores and distributes Docker images, more details here)

The benefit of using Docker Hub is that we only have to create an account there and the Oracle Container Cloud Service has per default access to that registry server, which means we don´t have to configure anything.

After creating an account on Docker Hub we have to login on our client using the Docker command line tools

docker login

Entering username and password of the Docker Hub account and we are done.

Next, we upload our Docker image to Docker Hub using again the command line tools:

docker tag db-apex-dev-image <docker-hub-username>/db-apex-dev-image
docker push <docker-hub-username>/db-apex-dev-image

(More details about Docker push can be found here)

Once the upload process finished, we can go back to our Oracle Container Cloud Service admin panel. (This process can take some time, dependent on your networking bandwidth. The image has about 15-16GB)
Note: Make sure that this image is publicly available, this can be configured in the Docker Hub web interface. If you want to have it private than you have to configure your Docker credentials in the Oracle Container Cloud Service admin panel.

Create a new Service

Within the Oracle Container Cloud Service admin panel we can now create a new service, which basically is a Docker run command. So we define how our Docker image would be configured to run. (e.g port mappings, temp file size, …)

The most simple and fastest way is to use the Docker Run Command editor and paste in a docker run config like this:

docker run \
--name=db-apex-dev-container \
-p=2222:22/tcp \
-p=8080:8080/tcp \
-p=1521:1521/tcp \
-v=/dev/shm \
-e="occs:description=Oracle Database and APEX Developer Docker Service" \
"<docker-hub-username>/db-apex-dev-image" \
--tmpfs /dev/shm:rw,nosuid,nodev,exec,size=2g

This defines:

  • HTTP port mapping: 8080 tcp
  • DB port mapping: 1521 tcp
  • SSH port mapping: 2222 tcp
  • Volume: /dev/shm
  • Volume used as tmpfs with the size of 2GB

After we have done that, all the necessary input fields of the create window are automatically filled. Only the service name input have to be set by hand. Here are some screen shots which describe the procedure.

Finally we have our Container Service defined and now we will see it up and running. The only thing we have to do now is to press the green Deploy button.

Now Oracle fetches our Docker image from Docker Hub and runs it with our configured run parameters. This step takes a few minutes to complete.

Connect to Services

To get the public address of our running services we have to go to the hosts detail page, this IP address is used for all containers/services running in this particular Container Cloud Service. This means all network ports which are defined, can only be applied once.

Lets say our Public IP address is then the services would be accessible like:


sqlplus system/oracle@


ssh root@ -p 2222

Note: These are the default port mappings as we defined in the docker run section above. The users and passwords are defined before building the docker image, read more about it on GitHub.
For security reasons the DB port should not be available publicly (maybe over SSH tunneling) and Tomcat should run over SSL

That´s it! 🙂 We are done and have a up and running Docker image in the Oracle Cloud packed with all the software components we like…