Week 11

Deployment Strategies

Creating Docker Images

Multi-stage builds

Docker Compose

Container Registries

Deploying to the Cloud

Database Migrations

Practice

Assignment

Backend Track

One command for the whole stack

In previous chapters, your Spring Boot application ran in a container, but every database call would fail. Inside a container, localhost is the container itself and it does not have Postgres installed on it. So your app was looking for PostgreSQL in the wrong place.

You could fix this by hand, with Docker network commands and several long docker run commands that you have to type in the right order, every time. Nobody does that. Instead, you describe your whole local setup โ€” the app and the database โ€” in one file, and start it with one command. That tool is Docker Compose, and it is already included in Docker Desktop.

Watch: What is Docker Compose?

https://www.youtube.com/watch?v=f3xb7kt_dH4

The Compose file

Create a file called compose.yaml in the root of your project, next to your Dockerfile. (You will also see the older name docker-compose.yml in many projects โ€” both work.)

<aside> ๐Ÿ’ญ

Compose files are written in YAML, which you used for GitHub Actions in the core program. Rusty? Review Appendix 1: YAML syntax.

</aside>

Here is a complete file for a Spring Boot API with a PostgreSQL database:

services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/mydb
      SPRING_DATASOURCE_USERNAME: dev
      SPRING_DATASOURCE_PASSWORD: dev
    depends_on:
      - db

  db:
    image: postgres:18
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
    ports:
      - "5432:5432"
    volumes:
      - db-data:/var/lib/postgresql

volumes:
  db-data:

Walking through it:

<aside> โ—

Look at the SPRING_DATASOURCE_* variables. Spring Boot automatically matches environment variables to properties: SPRING_DATASOURCE_URL overrides spring.datasource.url. So your application.properties keeps localhost for running the app directly on your machine, and the Compose values override it inside the container. Same image, different configuration โ€” this idea carries the rest of the week.

</aside>

<aside> โ—

Pin versions like postgres:18 instead of using latest. With latest, your project can break the day a new major version is released.

</aside>

Networking: how db becomes an address

When Compose starts your stack, it creates a private network for the project and connects every service to it. On that network, each service name is a hostname: Docker's built-in DNS makes db resolve to the database container.

That is why the datasource URL says jdbc:postgresql://db:5432/mydb. This is the fix for the previous chapter's problem: between containers you use the service name, never localhost.

And the ports: "5432:5432" on the db service? That is only for you: it lets the tools on your machine (psql or a GUI tool) connect to localhost:5432 and look inside the database. The api container does not need it.

<aside> โŒจ๏ธ

Hands on: Delete the ports: lines from the db service and run the stack again. Does the API still reach the database?

Volumes: keeping your data

In the previous chapters you mounted a folder from your machine with -v (a bind mount). The db-data volume here is a named volume: storage that Docker creates and manages for you. Named volumes are the standard choice for databases.

It is declared once at the bottom under volumes: and mounted into /var/lib/postgresql โ€” the folder where PostgreSQL stores its data. Result: your data survives containers being stopped, removed and recreated.

<aside> โš ๏ธ

docker compose down keeps your volumes. docker compose down -v deletes them โ€” that flag wipes your database. Use it only when you want a truly fresh start.

</aside>

Running the stack

To build and run all the containers, run the following command:

docker compose up

Docker pulls the Postgres image, creates the network and the volume, then starts both containers. Your API and your database in one command.

Other useful docker compose commands:

docker compose up            # build if needed, start everything, stream all logs
docker compose up -d         # the same, but in the background
docker compose ps            # what is running in this project
docker compose logs -f api   # follow the logs of one service
docker compose down          # stop and remove containers + network (volumes stay)

<aside> โ—

The modern command is docker compose (with a space). Many older tutorials show docker-compose (with a hyphen) - that is the legacy version. If a tutorial uses the hyphen, you can remove the hyphen when following the tutorial.

</aside>

<aside> โš ๏ธ

Changed your Java code but the container behaves like before? Compose reuses the existing image โ€” it does not rebuild automatically. Run docker compose up --build after code changes.

</aside>

A fresh database is empty

The PostgreSQL in your container is brand new โ€” it has none of your tables. Two ways to load your schema:

  1. By hand: connect your GUI tool or psql to localhost:5432 (that is what the published port is for) and run your schema SQL.
  2. Automatically: the Postgres image runs any .sql file it finds in /docker-entrypoint-initdb.d/ โ€” once, on the very first start with an empty volume. Mount your schema file into it:
services:
  db:
    volumes:
      - db-data:/var/lib/postgresql
      - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql

<aside> ๐Ÿ’ก

Added the init script but nothing happened? It only runs when the volume is empty. Your db-data volume already contains data from an earlier start โ€” run docker compose down -v first (this deletes the old data), then docker compose up again.

</aside>

Compose in the real world

Docker Compose is the standard way developers run a full stack locally: app, database and anything else, up in one command, identical on every teammate's machine.

In production it is a different story. Most companies run containers on managed cloud platforms or orchestrators (such as Kubernetes), which handle things Compose does not: multiple machines, automatic restarts and scaling. On a small scale โ€” one server running one application โ€” Compose is a perfectly valid production choice too.

For you this week, the split looks like this: the Compose file stays on your machine for development, and in the cloud your app becomes a single deployed container that connects to a managed database service. You will see exactly that in the Deploying to the Cloud chapter.

Healthchecks

<aside> ๐Ÿ’ญ

This section cover an advanced topic, it is optional to read.

</aside>

Sometimes docker compose up may show the api crashing with a database connection error โ€” and running it again works fine. What is going on?

depends_on controls the start order, not readiness. PostgreSQL was started first but was still initialising when the API tried to connect. Restarting works because by then the database is ready.

Make the API wait until the database is ready

Give the database a healthcheck and make api wait for it to pass:

services:
  db:
    image: postgres:18
    # ...same as before...
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev -d mydb"]
      interval: 2s
      timeout: 2s
      retries: 10

  api:
    build: .
    # ...same as before...
    depends_on:
      db:
        condition: service_healthy

Now Compose starts the API only after PostgreSQL reports that it is ready to accept connections.

Extra resources

Reading

Videos


The HackYourFuture curriculum is licensed underย CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

CC BY-NC-SA 4.0 Icons

Built with โค๏ธ by the HackYourFuture community ยท Thank you, contributors

Found a mistake or have a suggestion? Let us know in the feedback form.