Containers, Local Development, and Deployment Artifacts

LESSON

Backend Development Foundations

013 25 min beginner

Containers, Local Development, and Deployment Artifacts

By the end of this lesson, you will be able to...

  • Explain what a container image preserves about a backend runtime.

  • Distinguish a local development stack from the deployable application artifact.

  • Review an image, entrypoint, and readiness check for common deployment failures.

Idea in one sentence: A container image packages the backend runtime and startup command, while configuration, secrets, external services, and readiness evidence stay at runtime boundaries.

Core Insight

The previous lesson separated code from deploy-time configuration.

Now imagine the orders API has correct code and valid production configuration. CI passes. The release deploys.

Then the service crashes on startup:

error: libpq.so.5: cannot open shared object file

The database driver needs a native PostgreSQL library. That library exists on developer laptops. It exists in one CI job. It does not exist inside the production image.

The code did not change. The configuration did not point at the wrong environment. The runtime package was incomplete.

This is the problem containers are meant to narrow.

A backend is not only source code. It depends on:

The naive idea is:

If it runs on my machine, deployment should be fine.

Containers improve that story by packaging more of the runtime into an artifact. But they do not make production automatic. Secrets should not be baked into the image. Databases usually live outside the image. Readiness checks still need to say whether the process can serve.

The better model is:

image -> packaged runtime artifact
container -> running instance of that artifact
runtime config -> values injected by the environment
dependency services -> external systems the app talks to
readiness -> evidence that this instance should receive traffic

The Small Situation

Use one service: orders-api.

It talks to:

PostgreSQL
orders queue
payment provider

Developers run a local stack:

orders-api
postgres
redis
fake-payment-provider

Production deploys an application image:

orders-api:<commit-sha>

The visible pieces are:

Plain meaning:

An image is the thing you build and move around.

In this scenario:

orders-api:abc123 contains the application runtime, installed dependencies, application files, and startup metadata.

Technical name:

That is a container image.

Plain meaning:

A container is the image actually running as a process with runtime settings.

In this scenario:

The platform starts orders-api:abc123, injects DATABASE_URL, and gives it network access.

Technical name:

That is a running container.

A Build-to-Run Trace

Trace one release:

Input:
  git commit abc123
  build recipe
  dependency lock files

Step 1: CI builds image
  base runtime image is selected
  OS packages are installed
  language dependencies are installed
  application files are copied
  build or compile step runs
  entrypoint metadata is set

Intermediate state:
  image orders-api:abc123 exists

Step 2: tests check the artifact or equivalent output
  unit tests run
  integration tests run with test services
  startup config validation can be exercised

Step 3: image is pushed to registry
  tag or digest identifies the artifact

Step 4: production pulls the image
  platform creates a container from orders-api:abc123
  runtime configuration and secrets are injected

Step 5: entrypoint starts
  ./bin/orders-api serve --host 0.0.0.0 --port 8080

Step 6: app validates configuration
  config from lesson 012 is parsed and checked

Step 7: readiness check passes
  platform sends traffic to the instance

Output:
  production traffic reaches a known runtime artifact

Now compare the broken path:

CI:
  tests run on host machine
  host has libpq installed

Production:
  image is built separately
  image does not include libpq
  container starts
  process crashes before readiness

The missing evidence was:

Does the deployable image contain the runtime assumptions the app needs?

Testing source files on a host machine is useful. It is not the same as testing the artifact you deploy.

Check: CI tests source code on the host machine, then production builds a fresh image from a different base image. What evidence is missing?

Think first, then reveal.

Answer: Evidence that the deployable image itself works. If production runs a container image, the pipeline should test that image or the same build output that becomes the image.

Local Development Is Not the Artifact

Local development stacks are useful.

A compose.yaml or similar file might start:

orders-api
postgres
redis
fake-payment-provider

That helps developers run the system without manually installing every service.

But the local stack is not the production artifact.

The application image is one artifact. PostgreSQL, Redis, and the fake payment provider are support services. Production may use a managed database, a cloud queue, and a real payment API.

Two mistakes come from confusing these:

First, baking dependency services into the app image.

bad image:
  orders-api process
  PostgreSQL server
  queue server
  test payment fake

A web API image should usually run the web API process. External services have different lifecycles, backups, scaling needs, access controls, and failure modes.

Second, treating local convenience as production readiness.

Local development may:

Those are fine when explicit. They are dangerous when they silently become the deployment artifact.

Check: Should the production orders-api image contain production database files so the app and database always move together?

Think first, then reveal.

Answer: Usually no. The app image and database state have different lifecycles. The database needs backups, migrations, storage, access control, and operational handling outside the app image.

Entrypoints, Readiness, and Shutdown

The entrypoint is the command the platform starts.

For the web API:

./bin/orders-api serve --host 0.0.0.0 --port 8080

For a worker:

./bin/orders-api worker --queue orders-production

Entrypoints matter because the deployment platform manages the process. If the entrypoint starts a child process in the background and exits, the platform may think the app stopped. If it catches errors and keeps running, the platform may think the app is healthy when startup failed. If it ignores shutdown signals, deploys may drop in-flight requests.

A good production entrypoint should:

Readiness is different from liveness.

liveness:
  should the platform restart this process?

readiness:
  should this instance receive traffic?

For orders-api, readiness might require:

config loaded
server accepts requests
database connection is usable enough
required migration version is compatible

A bad readiness check says:

process exists -> ready

That can route traffic to an instance that started but cannot connect to the database. A readiness check that is too deep can also be harmful if it depends on a slow optional service. The right readiness check should reflect what this service must have to answer its core requests.

Artifact Review Table

When reviewing a deployment artifact, ask which boundary each item belongs to.

Item                          Belongs in image?        Runtime boundary?
application code              yes                      no
language runtime              yes                      no
native library needed to run   yes                      no
production API key             no                       secret injection
DATABASE_URL                   no                       config injection
debug shell                    usually no               emergency tooling elsewhere
PostgreSQL data files          no                       database service
fake payment provider          no                       local/test dependency
entrypoint command             metadata                 platform starts it
readiness check                app/platform contract     traffic decision

This table is not a universal law. Some debugging tools may be included intentionally. Some local images may be different from production images. The important habit is to make the decision explicit.

The deployment artifact should also be traceable:

source commit: abc123
image tag: orders-api:abc123
image digest: sha256:...
build time: 2026-07-01T10:15:00Z

latest is convenient for local experiments. It is weak release evidence in production because it does not identify exactly what is running.

Incident Signals

When a containerized backend fails, ask which boundary produced the bad evidence.

Use a small incident table:

Symptom                         Likely boundary to inspect
process cannot find library      image build
config validation fails          runtime configuration
secret appears in image history  artifact/secret boundary
wrong server command starts      entrypoint
traffic reaches broken instance  readiness
old code and new image unclear   traceability
container restarts repeatedly    startup, liveness, or crash loop
slow shutdown drops requests     signal handling

This table keeps diagnosis concrete.

If the process cannot find libpq, inspect the image build. Did the build recipe install the native package? Did tests run against the same image?

If startup says DATABASE_URL is missing, inspect runtime configuration. The image may be fine. The environment did not inject the value the app needs.

If traffic reaches an instance that cannot talk to the database, inspect readiness. The process may be alive, but the platform trusted it too early.

If nobody knows which code is running, inspect traceability. A digest, commit SHA, build ID, and deployment event should let operators connect the running container back to source and pipeline evidence.

The useful habit is to avoid one vague sentence:

Docker is broken.

Replace it with a boundary question:

Did the image, runtime config, entrypoint, readiness check, or release identity lie?

Trade-offs and Limits

Containers buy runtime reproducibility. More of the backend's runtime assumptions travel with the artifact.

They cost build discipline. Images can become large, slow to build, full of unnecessary packages, or hard to debug if the build recipe is unclear.

The main trade-off is reproducibility versus image complexity and attack surface.

Smaller images transfer faster and usually include fewer unnecessary tools. Larger images may be easier to inspect during debugging, but they carry more packages and more things to patch.

Pinned base images make builds more repeatable, but they need planned updates. Floating tags are convenient, but they can change without source code changing.

Containers do not replace:

They make the runtime package easier to reason about. Other boundaries still exist.

You can see the boundary is weak when:

Practice

Review this deployment artifact:

Image:
  tag: orders-api:latest
  includes source code, runtime, dev compiler, debug shell
  has PAYMENT_API_KEY baked into image at build time

Entrypoint:
  ./start-dev-server

Health:
  readiness returns 200 if process is running

Deployment:
  production injects DATABASE_URL at runtime

A good answer should mention:

Model answer:

This artifact should not be promoted as-is. The tag does not identify the exact release, the image contains a secret, the entrypoint suggests a development server, and readiness can send traffic to a container that cannot serve real requests. A safer shape uses orders-api:<commit-sha> or an immutable digest, excludes production secrets, injects configuration at runtime, starts the production command, validates config at startup, and reports readiness based on the service's real requirements.

Connections

Lesson 012 explained why configuration and secrets belong at runtime boundaries. This lesson applies that idea to images: the image should package the runtime, not production secrets or external service state.

The next lesson uses this artifact as one part of a release pipeline: build once, test the artifact, coordinate migrations, roll out, and stop when checks fail.

Resources

Key Takeaways

PREVIOUS Configuration, Secrets, and Twelve-Factor Boundaries NEXT CI/CD, Migrations, and Release Checks