Containers to serve configuration-friendly applications
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.
TL;DR
Just use environment variables and pass them to the container.
Long version
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.
What is an environment variable?
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.
What kind of information my app should grab from it?
External information that changes runtime behavior are a great start:
- build and/or version tags
- database address and credentials
- service api keys
- feature flags
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.
How my application can externalize things?
Most languages and frameworks already have neat ways to recover info from external resources:
- spring-boot cascades environment variables into it's properties in a transparent way
- dotenv-flow performs similar job on backend node js projects
- vite and similar tools do the same for frontend development
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.
Building configuration-friendly images
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:
- the
Dockerfileusually gets versioned along the rest of the source code. Do not save sensitive data (like the database connection url) here. - The variables can be used during image build and container runtime. To be more specific, you can declare your variables on Dockerfile, use them as-is to build the image and then override any of them before start the container.
- If some variable is needed ONLY during build time, it's better to use build-args.
The image build command follows:
# from the root directory, which usually has the Dockerfile
docker build -t sombriks/simple-knex-koa:latest .
Running the image
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.
Some orchestration
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.
Cool Tools to deal with containers
Intellij Ultimate has a nice plugin to deal with images, containers and registries:

Conclusion
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!