---
title: "05 — Using Third-Party Images: Languages, Databases, and the Twelve-Factor App"
uid: using-third-party-images
tags: ["architecture", "roadmap:docker", "docker", "twelve-factor", "databases", "images", "microservices"]
excerpt: "A third-party image is a pre-configured process — and the rules that make containerized apps behave are the Twelve-Factor principles, not tooling."
date: 2026-08-13T03:28:14+0000
source: https://www.aveshina.my.id/en/blog/using-third-party-images
---

"Just docker pull stuff" was my third-party-image strategy, and it conflated three uses that are nothing alike. The frame that separated them is about architecture, not tooling: **a third-party image is a pre-configured process, and the design rules that make containerized apps behave well — stateless processes, externalized config, disposable lifecycles — are the Twelve-Factor App principles.** [1][4]

The framing worth keeping is that "using third-party images" spans very different jobs. Pulling a language runtime to base my own image on, pulling a database to run as infrastructure, and pulling a one-shot CLI utility are three distinct patterns that happen to share one verb (pull) [1]. The skill is recognizing which one I'm doing, because each has a different design rule attached.

## Three uses, three patterns

```figure
<svg viewBox="0 0 720 250" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Three columns. Left: a base image like node:20 with FROM on top of it, used to build a custom app image. Middle: a database image like postgres:16 with a volume attached underneath, used as infrastructure. Right: a CLI utility image run with --rm to do one job and exit. Each has a different lifecycle.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- Headers -->
    <text x="120" y="22" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">Language runtime</text>
    <text x="120" y="37" font-size="10" fill="#64748b" text-anchor="middle">base for my own image</text>
    <text x="360" y="22" font-size="12" font-weight="700" fill="#422006" text-anchor="middle">Database</text>
    <text x="360" y="37" font-size="10" fill="#64748b" text-anchor="middle">infrastructure with state</text>
    <text x="600" y="22" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">CLI utility</text>
    <text x="600" y="37" font-size="10" fill="#64748b" text-anchor="middle">run once, exit</text>

    <!-- Base image -->
    <rect x="40" y="60" width="160" height="40" rx="6" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="120" y="84" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">node:20 (base)</text>
    <rect x="40" y="108" width="160" height="40" rx="6" fill="#c7d2fe" stroke="#6366f1" stroke-width="1.5"/>
    <text x="120" y="132" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">my-app code + deps</text>
    <text x="120" y="170" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">FROM node:20 → build on top</text>

    <!-- DB -->
    <rect x="280" y="60" width="160" height="50" rx="6" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="360" y="82" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">postgres:16</text>
    <text x="360" y="98" font-size="9.5" fill="#475569" text-anchor="middle">configured process</text>
    <rect x="280" y="120" width="160" height="30" rx="6" fill="#fde68a" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="360" y="139" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">volume (data lives here)</text>
    <text x="360" y="170" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">state on a volume, not in the image</text>

    <!-- CLI -->
    <rect x="520" y="60" width="160" height="50" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="600" y="82" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">some-tool:latest</text>
    <text x="600" y="98" font-size="9.5" fill="#475569" text-anchor="middle">docker run --rm …</text>
    <text x="600" y="150" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">does its job, --rm cleans up</text>
  </g>
</svg>
```

The three columns have different lifecycles, and conflating them is what produced my early messes. Each one deserves a sentence.

## Languages: the base image

A language image (node:20, python:3.12, golang:1.22) is a starting point I build *on top of* [2]. My Dockerfile's FROM node:20 pulls that image and stacks my app's layers above it. Knowing my language matters here, because the efficiency of the final image depends on choices only a language-native would make: multi-stage builds for Go (compile in a fat image, copy the binary into a scratch image), Alpine variants for Node/Python (smaller, but watch out for musl-vs-glibc native deps), layer ordering so npm ci re-runs only when package.json changes [2]. The roadmap's point about "knowing at least one language" is really about this — I cannot build a tight image without understanding my language's package model and runtime.

## Databases: infrastructure with a volume

A database image (postgres:16, mysql:8, mongo:7) is different in kind. I don't build on top of it; I *run it as infrastructure*, and the only thing I have to get right is where the data lives [3]. The image itself is stateless and throwaway — Postgres comes up, finds a data directory, and serves. The data has to be on a volume attached at docker run time, otherwise the moment I recreate the container my entire database is gone.

```
docker run -d \
  --name db \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16
```

The -v pgdata:/var/lib/postgresql/data line is the load-bearing part. It puts the data directory on a named volume Docker manages, so I can docker rm -f db and bring up a fresh postgres:16 container pointing at the same volume, and my tables are still there [3]. The image and the data are finally separated — which is the whole point of how containers handle state.

## CLI utilities: run once, exit

The third use is the one I underused for a long time. Any CLI tool shipped as an image can be run ad-hoc without installing it on my host [1]:

```
docker run --rm -v "$PWD:/work" -w /work mikefarah/yq eval '.a.b' config.yml
```

The --rm removes the container the moment it exits, so the tool leaves no trace on my system. I get a clean, version-pinned toolchain per command without polluting my laptop. This is the pattern behind running formatters, linters, scanners, or migration tools from CI without a global install step.

## Application architecture: the Twelve-Factor rules

Once I had the three uses straight, the *design* question became interesting: how do I structure an app so it actually benefits from containers? The roadmap points at microservices and container design patterns, and the cleanest distillation I found is the **Twelve-Factor App** methodology [4][5]. It's a list of principles written in 2011 — before Docker — that containerization made the default way to build:

- **One codebase, many deploys.** The same image runs in dev, staging, prod.
- **Explicit, externalized configuration.** Config lives in environment variables, never baked into the image. docker run -e DATABASE_URL=… is the Twelve-Factor config rule made literal.
- **Backing services treated as attached resources.** A Postgres container, a managed RDS instance, and a third-party SaaS API are all "attached resources" — the app talks to them the same way, by URL.
- **Processes are stateless and share-nothing.** Any state that needs to survive goes in a backing service (database, object store), never in the container's memory or writable layer [5].
- **Disposability.** Containers start fast and shut down on SIGTERM cleanly, so the orchestrator can move them around.

The insight for me was that containers didn't invent these rules — they *enforce* them. A stateful container is a contradiction; a config-baked image can't be promoted between environments. The technology simply makes bad architecture painful fast, which is its real value.

## How I use this

The payoff is a triage habit when I reach for an image. If I'm extending a runtime, I treat the image as a base and obsess over my own layers' size and cache order. If I'm running infrastructure, the first thing I wire up is the volume — the image is irrelevant if the data isn't persistent. If I'm running a one-shot tool, I reach for --rm and never think about it again. And when I design the app itself, I run the Twelve-Factor checklist: is config externalized, is state in a backing service, will it die cleanly on SIGTERM? Containers reward that design and punish its absence, which is why the rules finally feel less like opinion and more like physics.

## References

[1] Docker, Inc., "Docker Hub Registry," hub.docker.com, 2024. [Online]. Available: [https://hub.docker.com/](https://hub.docker.com/)

[2] Docker, Inc., "Building images — concepts," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/get-started/docker-concepts/building-images/](https://docs.docker.com/get-started/docker-concepts/building-images/)

[3] Docker, Inc., "Use case — Run a database in a container," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/guides/use-case/databases/](https://docs.docker.com/guides/use-case/databases/)

[4] A. Wiggins, "The Twelve-Factor App," 12factor.net, 2017. [Online]. Available: [https://12factor.net/](https://12factor.net/)

[5] microservices.io, "Microservices Architecture," microservices.io, 2024. [Online]. Available: [https://microservices.io/](https://microservices.io/)

[6] Kubernetes, "Container Design Patterns," Kubernetes Blog, 2016. [Online]. Available: [https://kubernetes.io/blog/2016/06/container-design-patterns/](https://kubernetes.io/blog/2016/06/container-design-patterns/)

```quiz
Q: When you `docker run postgres:16`, where must the data live for it to survive a container recreation?
- In the image's writable layer
- On a volume mounted at the data directory
correct: 1
explain: The image and its writable layer are throwaway. The data must be on a volume (-v pgdata:/var/lib/postgresql/data) so a fresh container pointing at that volume sees the same tables.

Q: A language image like node:20 is typically used as…
- infrastructure run alongside the app
- a base that my own Dockerfile builds on top of with FROM
correct: 1
explain: Language runtimes are base images. My app's code and dependencies are layered on top via FROM and subsequent instructions.

Q: What does `docker run --rm` do for a CLI utility image?
- Keeps the container running forever
- Removes the container the moment it exits, leaving no trace
correct: 1
explain: --rm deletes the container after it exits. It's how I run version-pinned CLI tools ad-hoc without polluting the host with installs.

Q: The Twelve-Factor rule that says "config lives in environment variables, never in the image" maps to which Docker practice?
- Baking DATABASE_URL into the Dockerfile with ENV and never changing it
- Passing -e DATABASE_URL=... at docker run time so the same image runs in any environment
correct: 1
explain: Twelve-Factor config is externalized. The same image is promoted across environments by changing the env vars at run time, not by rebuilding.

Q: Why are containers said to 'enforce' Twelve-Factor design rather than invent it?
- Because container images are immutable, so any state or config baked in forces a rebuild — disposability and externalized config stop being optional
- Because the OCI spec mandates Twelve-Factor compliance
correct: 0
explain: The principles pre-date Docker. Containers make them hard to violate: state in a writable layer is lost on removal, config baked into an image blocks promotion, and slow shutdowns break orchestration.
```
