One nice thing about containers is they kinda standardized how applications should be distributed. You still need to understand the specifics of adopted technology stack, yes, but when it comes to put it to run, just create an image, publish it into some registry and then spin that image somewhere. We already talk about that here and here.
But to get the state of the art, your container image must not be an opaque and hard to get inside coconut, it must offer some flexibility and be somewhat configurable from the outside world.
Just use environment variables and pass them to the container.
Most modern applications will end up having to deal with some degree of state management, and solutions like queues, databases and event brokers will take place.
Those components are usually external to the application and need configurations to proper serve us.
And even if not mandatory, it is a best practice to make those configurations sensible to the current application environment.
For all those needs, all modern operating systems offers a solution: environment variables.
They are one of the ways that the current shell has to pass runtime information to a child process.
If your application needs external information, it can receive input from user, query the network or the filesystem or check variables from parent process, usually the shell.
External information that changes runtime behavior are a great start:
Note also that kind of externalization precedes all that container stuff in decades. To get your configuration externalized is a common practice since the dawn of modern age computing.
Most languages and frameworks already have neat ways to recover info from external resources:
That kind of development style implies sometimes tha a application profile or application mode exists and there is some specifications about that, like the twelve factor app methodology.
To build a configuration-friendly image we start with a configuration-friendly application, as we debated in previous section.
In this example we can see a reasonable configurable application depending on these key environment variables:
# also depends on NODE_ENV
ALG=aes-256-cbc
SECRET=CH4NG3M3CH4NG3M3CH4NG3M3CH4NG3M3
DEBUG=knex:query,knex:bindings,koa:*
A Dockerfile to package this app would be something like this:
FROM node:18-alpine
# some useful description
LABEL name=simple-knex-koa-example \
description="small koa.js service consuming database using knex.js"
# files needed to proper build and run this.
ADD index.mjs package.json .env.production .env.test /app/
# mind the trailing '/' in app/, it's important!
ADD app/ /app/app/
# switching for our working directory inside de image filesystem
WORKDIR /app/
# environment configuration.
ENV PORT=3000 \
NODE_ENV=production \
PG_CONNECTION_URL='please configure database url connection properly'
# informing the port this image will expose to the outside world
EXPOSE $PORT
# install deps and show image folder structure so it can be checked on logs
RUN npm install; echo "some results: "; pwd; ls -la; ls -la app # ; npm run test
# how this app runs
ENTRYPOINT npm start
Important things to note:
Dockerfile
usually gets versioned along the rest of the source code. Do
not save sensitive data (like the
database connection url) here.The image build command follows:
# from the root directory, which usually has the Dockerfile
docker build -t sombriks/simple-knex-koa:latest .
Now, run the application image and replace the variables accordingly:
docker run --name sample-container -p 3000:3000 \
-e PG_CONNECTION_URL=postgres://username@password@host/database_name \
-d sombriks/simple-knex-koa:latest
There!
This is how you override your sensitive configuration.
Container orchestration is one of the modern design patterns for microservices and containerized applications.
It goes one step forward application packaging and starts to define how different applications can collaborate with each other.
Using a docker-compose file, it's possible to not only indicate how to find out a postgresql database, but provide one to team up with our application. See example file bellow:
version: "3.5"
services:
knex-koa-app:
# build: .
image: sombriks/simple-knex-koa
environment:
PG_CONNECTION_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db/${POSTGRES_DB:-books}
ports:
- "3000:3000"
expose:
- 3000
healthcheck:
test: [ "CMD", "wget", "-S", "--spider", "http://127.0.0.1:3000/status" ]
interval: 30s
timeout: 30s
retries: 30
restart: on-failure
depends_on:
db:
condition: service_healthy
# https://hub.docker.com/_/postgres
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-books}
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
ports:
- "5432:5432"
expose:
- 5432
healthcheck:
# https://www.postgresql.org/docs/9.4/app-pg-isready.html
test: [ "CMD-SHELL", "pg_isready" ]
interval: 30s
timeout: 30s
retries: 30
restart: on-failure
This also samples a bit about observability, since we have restart and healthcheck operations.
Most orchestration solutions try to offer such commodities so we not only run packaged applications, but make sure they will keep running and answering when the bell rings.
Intellij Ultimate has a nice plugin to deal with images, containers and registries:
In this example we used a node application, but the same applies to other languages and stacks.
If your application was configurable already then it's a matter of write the packaging stuff for it, publish the image (or build locally) and provision some infrastructure to it.
See you in the nex article, happy hacking!