Docker for the Sanctity of Your Dev Environment
A developer’s own laptop —our local development environment for all our projects, is a sanctum. Cleanly installed IDEs, enhanced terminal with updated package managers installing everything in into neatly isolated projects, Git repos, ssh keys and credentials configured for all your staging and production environments. Until a project comes along with a dependency that wants to cast a big footprint on your system with drivers and extra software. It’s usually something big and hefty like a non-open-source database. It’s Oracle. I’m talking about Oracle for this article.
Oracle is an un-rivaled database system with decades of development in its foundation. I have no alternative for an existing database I need to connect to, I need to have the client on my local development system and I’ll need to install it onto any system that that my project will be deployed to.
There is a lightweight option, however — the Oracle Instant Client. This can maybe be installed to my system using a package manager, but I only need it for this one project. I’d rather not let it make itself a nest on my system and stay there.
Docker was designed for this.
I’m going to make this work cleanly using Docker in three steps:
- Build a Docker image that will include the Oracle Instant Client.
- Connect to my database from the Docker container.
- Use the Visual Studio Code Remote Containers extension to build my app and connect it to my Oracle database from within the Docker container.
To follow along, you’ll only need general familiarity with how Docker works, and the Oracle stuff is specific to my example. These same steps can generally apply to any complicated dependency you come across in your projects.
Build a Docker Image from Oracle Linux
My project will begin with a single Dockerfile
that will start off surprisingly simple. For now, I just want to get Oracle SQL*Plus to run from a Docker container.
Oracle’s site for Instant Client points to Docker files available on Github. There’s options for a few different versions, and it’s part of a massive Git repo that you don’t need to clone. Download their Dockerfile
to your project, or even just copy the few simple lines:
This will build a docker image based on Oracle Linux 8-slim, and will use the built-in package manager to install Oracle Instant Client along with the SQL*Plus developer tool.
With Docker installed on my system, this simple text file serves as instructions to build a runnable ‘image’ of Oracle Linux with the instant client installed. This image and everything cached with it is all going to stay within my Docker installation. I’ll allow that.
~ docker build --pull -t oracle/instantclient .
This is a simple Docker command so far. We’re telling Docker to build an image from Dockerfile
in the present working directory, pull the latest dependencies if necessary and tag this image oracle/instantclient
so we can run it by name.
To test this out, we can run this image to create a quick little container that will execute the default command from the Dockerfile:
sqlplus -v
which should give us a quick line of output just so we know the Oracle client is installed.
~ docker run --rm oracle/instantclientSQL*Plus: Release 21.0.0.0.0 - Production
Version 21.1.0.0.0
This command tells Docker to run the image that we just built and tagged oracle/instantclient
, the --rm
signals that the container should be removed as soon as it exits after outputting the results.
Connect to the Database
Now that I have a Docker image with the database client installed on it, I need to actually connect to my database.
For Oracle, the really long connection descriptors usually go in a tnsnames.ora
file. I also need a sqlnet.ora
file and a private key for an Oracle ‘wallet’ in order to connect securely. These are normally kept in the Oracle client installation path, global to your whole system.
I want to move this to the project level. In my project directory (that still only has the one Dockerfile
), I’m going to create a subfolder oracle/network/admin
, a familiar file path for those who have used the Oracle client, and store all the files that the Oracle client needs from me in there.
{PROJECT}
oracle/network/admin
wallet
<wallet files>
tnsnames.ora
sqlnet.ora
Dockerfile
I need to pass those files into the Oracle client that’s going to run isolated within a Docker container. The way to do this is to use a bind mount so that this path from my project directory is mounted from a specific path in the filesystem of the Oracle Linux operating system that the Docker container hosts.
I need to know what path to use inside the container. The Oracle Instant Client for Docker documentation says the default path is /usr/lib/oracle/<version>/client64/lib/network/admin
, and there’s a quick way to check if that path already exists in the container: let’s run a shell terminal on it.
~ docker run -it --rm oracle/instantclient bash
bash-4.4# cd /usr/lib/oracle/21/client64/lib/network/admin
bash-4.4# ls
README
bash-4.4# cat README
====================================================================This is the default directory for Oracle Network and Oracle Client configuration files. You can place files such as tnsnames.ora, sqlnet.ora and oraaccess.xml in this directory.
NOTE:
If you set an environment variable TNS_ADMIN to another directory containing configuration files, they will be used instead of the files in this default directory.
====================================================================
The command run here is similar to what we ran before, except we’re specifying the command to run: bash
. That will give us a shell, and the -it
switch will make it interactive so we can type commands and the container won’t terminate until we exit the shell.
I navigated to the directory specified in the docs and the README
file waiting there confirmed that I have the right location.
Now I can run the docker command with the bind-mount path specified so that when Oracle looks in that /usr/lib/oracle21/client64/lib/network/admin
folder, it will find the files that are sitting in my project path under oracle/network/admin
.
~ docker run -it --rm --mount type=bind,source=$PWD/oracle/network/admin,target=/usr/lib/oracle/21/client64/lib/network/admin oracle/instantclient sqlplus
This docker command says to run sqlplus
interactively from a new oracle/instantclient
container (that will be cleaned up when we’re done) and bind $PWD/oracle/network/admin
on our computer to /usr/lib/oracle/21/client64/lib/network/admin
on the Oracle Linux filesystem in the running container. Docker requires absolute paths, so I’m relying on $PWD
to evaluate to the full path to my project directory.
This time, SQL*Plus is launched interactively, and I can log into the database that I’ve included in my tnsnames.ora
file.
One pitfall, specifically for this Oracle example — the sqlnet.ora
file might contain the absolute path to the wallet directory for Oracle to find your encryption keys. You’ll want to edit that file and fix the relevant entries so that they map to the correct path from within the running container.
Run VS Code From the Docker Container
The logical next step is to get building! You’re ready to scaffold out your app, install the Oracle libraries for whatever programming language you’re using, and probably get the Oracle Developer Tools extension for VS Code to make your life easier.
But all you have so far is a rudimentary terminal interface to this Docker container, making it feel like a remote server where you’ll have to leave your comfortable IDE behind. A Docker container isn’t a remote server, though. It’s not even a virtual machine even though it seems like it. A Docker container is a subsystem — it runs directly on your machine, just in processes that are completely isolated. If VS Code were somehow able to ‘proxy’ all it’s functionaly into the Docker image, there shouldn’t even be any latency.
This is now possible.
Install the Remote — Containers extension in Visual Studio Code. As of these screenshots, it’s in the preview stage, but this concept is way too powerful to be patient for a more stable release.
It will add a new section on the left side of the status bar that stands out:
Clicking that green area will open the command bar with the Remote-Containers commands to choose from.
Choose the command to ‘Reopen in Container’ → ‘From Dockerfile’ to generate a new configuration from that Dockerfile
I started with.
This will generate a new settings file in your project workspace: .devcontainer/devcontainer.json
, and then the whole VS Code window will go blank and restart itself. When it comes back, the same workspace is there, but all my regular extensions are gone.
The Remote Containers extension has taken my Dockerfile
, extended it with some extra commands that install a VS Code remote server, binds my workspace folder as a volume, and refreshes with the entire IDE proxied through the remote server. I can now install extensions that I’ll only need for this project, as well as sync over any of my normal extensions that I’ll also need.
An important note for installing extensions — if you click the button clearly labeled to install the extension in your dev container, it will actually install the extension on your local VS Code installation, and sync it to your remote dev container installation. If you only want the extension installed in this dev container, you’ll need to take an extra step to add it to your devcontainer.json
and then rebuild the dev container.
Note that in my specific case — using an Oracle Linux slim docker image rather than something more development-ready, I had to add some extra commands to my Dockerfile
to avoid errors by installing additional packages that VS Code or specific extensions depend on:
My VS Code workspace is now running from that Docker container. If I open the integrated terminal, it will be the bash shell from the container. If I go to the Oracle Developer Tools extension I just installed, it will find the Oracle Instant Client installed on the Docker container. The only thing I’m missing is the oracle/network/admin
bind mounting. That was a command-line parameter and now I’m letting the Remote Containers extension manage that.
In the devcontainer.json
file, uncomment the mounts
setting and copy the same exact argument we used when we ran Docker container earlier:
"mounts": [ "type=bind,source=${localWorkspaceFolder}/oracle/network/admin,target=/usr/lib/oracle/21/client64/lib/network/admin" ],
Instead of $PWD
we use ${localWorkspaceFolder}
since this will be resolved by VS Code.
Any time a change is made to the devcontainer.json
or the Dockerfile
, the command to rebuild the container needs to be run for the changes to take.
Next Steps
This project is ready to get going now. When you or anyone else clones this project and opens the workspace in VS Code, it will see the .devcontainer
settings and prompt you to reopen in the Dev Container. Then you’ll have your full IDE, feeling very native but running from the Docker container with all the system dependencies and special extensions you need to start developing quickly.
Depending on what kind of Docker image you start with, you may be installing a lot of packages and dependencies as you scaffold out your app. In my case, Oracle Linux has nothing but the Oracle Instant Client, so I’ll be installing everything from scratch.
Keep in mind that everything you add needs to be added to the Dockerfile, not just run directly on the container or it will be lost the next time you rebuild the container.
Docker Compose
In my case with this particular example, I’m not going to be running a local Oracle database in a docker container. However, for other projects using open source databases, it’s possible to set up a docker-compose.yml
file that will start up your Dockerfile
along with a database or whatever else you need from another Docker image. The Remote Containers extension can work with this.
Staging and Production
When you’re ready to deploy your project to a staging environment so that others can test it, your Dockerfile
has everything needed to run the app in a development environment. For the staging and production environments, you may have less dependencies needed, different environment variables and configurations, and most importantly — your app actually needs to be built. So far it’s just been bind-mounted directly from your code workspace to facilitate a cycle of rapidly rebuilding.
For deployment, depending on your type of project, you’ll probably need a separate Dockerfile
and a procedure to build your app and copy the output to your Docker image before pushing it to your Docker registry.
Clean Sweep
Finally, when it comes time to move on from the project, whether it’s just the end of the day, or it’s time to start on a new project, you can stop the container and you’re left with no trace of any of the stuff you had to install and configure to get the project working.