Skip to content

Final Project ​

At this point you have covered all the core topics of Docker Essentials: running containers, building images, storing data, working with networks and environment variables, and Docker Compose. Now it is time to prove that this knowledge sticks by working on the projects below.

How to work on them:

  • There are no step-by-step instructions. You design the solution and type the commands yourself, starting from the study case and the end goal.
  • Every project has a verification checklist. You succeed when all checklist items pass, not when your commands look the same as someone else's.
  • If you get stuck, open the clues. The clues are ordered from light to more specific. Try the first one before the others.
  • All projects use only the material from lessons 1 to 9. Links to the relevant lessons are always included.

Work on the projects in order, because each project uses the result of the previous one.

Project 1: A local database for development ​

Level: basic · Material: Working with containers, Working with images, Docker Volume, Environment Variable

Study case: your frontend team needs PostgreSQL for local development. Nobody wants to install PostgreSQL directly on their laptop because the version often conflicts with other projects. You are asked to set up a database container that the whole team can use, with two rules from the tech lead: the database password must not be hardcoded in scripts, and the data must stay safe even if the container is deleted.

End goal:

  • A PostgreSQL container runs in the background with the name dev-db.
  • The password and database name are provided through environment variables.
  • Port 5432 in the container is mapped to port 5433 on the laptop, so it does not conflict with another database later.
  • The data is stored in a named volume, not inside the container.

Verification checklist:

Clue 1

The postgres:16 image reads the environment variables POSTGRES_PASSWORD and POSTGRES_DB at first start. How to pass them is covered in the Environment Variable lesson.

Clue 2

For the safe data rule, run the container with the option -v <volume-name>:/var/lib/postgresql/data. This is the named volume pattern explained in the Docker Volume lesson. You can also list your volumes with the command shown in that lesson.

Clue 3

To add and check data, run psql inside the container with docker exec, in the format docker exec -it dev-db psql -U postgres -d <database-name>. The -it option and how docker exec works are covered in the Working with containers lesson.

Project 2: Dockerize an application ​

Level: intermediate · Material: Working with images, Building your own Docker image, Environment Variable

Study case: your team has a small service called status-api, written in Node.js. The problem: every new team member must install the right Node version before they can run it. You are asked to package this app into an image, so anyone can run it with one command and without installing anything.

The source code is only two files. Create a new folder, then save both files inside it.

server.js:

js
const http = require("http");

const port = process.env.PORT || 3000;
const message = process.env.MESSAGE || "Hello from container";

const server = http.createServer((req, res) => {
  res.end(`${message} (port ${port})`);
});

server.listen(port, () => {
  console.log(`status-api running on port ${port}`);
});

package.json:

json
{
  "name": "status-api",
  "scripts": {
    "start": "node server.js"
  }
}

This app reads two environment variables: PORT (the port the app listens on, default 3000) and MESSAGE (the text it returns, default Hello from container). This is intentional, so you can prove the container configuration yourself.

End goal:

  • A Dockerfile that copies both files into the image and runs the app with npm start.
  • The image builds successfully with the name status-api and the tag 1.0.
  • The container runs in the background, the port is mapped to the laptop, and the message shown in the browser comes from the environment variable you passed to docker run, not from the default.

Verification checklist:

Clue 1

The base image node:22-alpine is used in the example in the Building your own Docker image lesson, complete with the WORKDIR, COPY, and CMD pattern. You only need to adjust it for these two files.

Clue 2

The port inside the container stays 3000 for both containers. Only the laptop port is different. Remember the pattern -p <laptop-port>:<container-port>, and remember that the app reads PORT if you want to change the port inside the container too.

Clue 3

Do not forget .dockerignore. There is no node_modules in this project, but a .env file or a Git folder could be copied into the image through COPY . . if it exists. Read the .dockerignore part again in the Building your own Docker image lesson.

Project 3: An app + database stack with Compose ​

Level: advanced · Material: Docker Volume, Docker Network, Environment Variable, Docker Compose

Study case: status-api from Project 2 is growing up. The product manager wants every visit to the main page to be recorded in a database, so the number of visits can be checked at any time. The stack now has two services: api (an image you build yourself) and db (PostgreSQL). You are asked to run both as one stack with Docker Compose, with the configuration stored in the repo, not just a list of commands in a terminal.

Save the two files below in the same folder as the Dockerfile from Project 2, and update them.

server.js (the app now records every visit in PostgreSQL):

js
const http = require("http");
const { Pool } = require("pg");

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

const server = http.createServer(async (req, res) => {
  await pool.query(
    "CREATE TABLE IF NOT EXISTS visits (id SERIAL PRIMARY KEY, at TIMESTAMPTZ DEFAULT now())"
  );
  await pool.query("INSERT INTO visits (at) VALUES (now())");
  const result = await pool.query("SELECT count(*) AS total FROM visits");
  res.end(`Total visits: ${result.rows[0].total}`);
});

server.listen(process.env.PORT || 3000, () => {
  console.log(`status-api running on port ${process.env.PORT || 3000}`);
});

package.json (it now needs the pg dependency, the PostgreSQL driver for Node.js):

json
{
  "name": "status-api",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "pg": "8.12.0"
  }
}

This app reads the DATABASE_URL environment variable, which is the connection string to PostgreSQL. The format is: postgres://<user>:<password>@<host>:<port>/<database-name>.

End goal:

  • A compose.yaml with two services: api, built from your own Dockerfile, and db, which uses the postgres:16 image.
  • The api service connects to PostgreSQL through the service name as hostname, not localhost.
  • The PostgreSQL password is read from a .env file, not written directly in compose.yaml.
  • The visit data is stored in a named volume, so it stays there after the stack is taken down and started again.

Verification checklist:

Clue 1

The build field in compose.yaml makes Compose use your own Dockerfile, and depends_on makes the db service start first. Both are covered in the Docker Compose lesson. Remember the note from that lesson: depends_on only controls the start order. If the first page load fails, the app tried to connect before PostgreSQL was ready. Wait a moment, then refresh.

Clue 2

For DATABASE_URL, replace the host part with the service name db, for example postgres://postgres:${POSTGRES_PASSWORD}@db:5432/app. The ${...} mechanism from the .env file is also covered in the Docker Compose lesson.

Clue 3

For the database volume, look at the db service in the Compose example in the Docker Compose lesson: the named volume is mounted at /var/lib/postgresql/data, and the list of volumes is declared in the volumes key at the bottom of the file. After that, down without the -v option will not delete your data.


Did you pass every checklist item? You have proven that you can use Docker for real work: running services with the right configuration, packaging an app into an image, and connecting several services with Compose. Congratulations 🥳

If some checklist items still fail, do not look at someone else's solution right away. That is exactly where you learn the most: read docker logs, check docker compose ps, and compare with the relevant lessons.