Learning paths / Containers / Containerize a real app

Try it: containerize a provided app

Reading · 30 min · Module 8, lesson 5 of 635 min left in this module

Module 8 · Containerize a real appLesson 5 of 6

Goal: Containerize an unfamiliar app end to end, from its README to a small, non-root image that answers /healthz.

3:49 · captions and chapters · narrated with an AI-generated voice
Transcript

Narration uses an AI-generated voice.

[00:00] Where we're going

By the end of this video, an app you've never seen will be running in a container, and answering its health check. We'll take the Node sample, and work from its README, one step at a time. An ignore file. A Dockerfile with two stages. Then we build it, run it, and ask it if it's healthy.

[00:21] Read the README

Before writing anything, read the README, and find three facts. First, the port. It listens on three thousand, or on whatever the port variable says. And it binds every address, not just localhost, so it's reachable from outside the container. Second, the health endpoint. It's slash health z, and it answers status okay. Third, how it starts. The README says npm start, which runs node server dot js. Every choice in the Dockerfile follows from those three.

[00:56] Ignore file and stage one

Next, the ignore file. It lists what must never reach the build. Your node modules folder was built for your machine. The image installs its own. Any env file holds secrets. And the git folder is history the app doesn't need.

Now the Dockerfile, in two stages. The first one installs the dependencies. It starts from Node twenty-two slim, and copies in only the package files. Then it installs exactly what the lockfile lists, without the development tools. Because the package files come first, that install stays cached until your dependencies change.

[01:39] Stage two

The second stage is what ships. It starts from a distroless Node image. That's Node, and nothing else. No shell, and no package manager. It copies in just two things. The installed modules from stage one, and the server file. It sets the port to three thousand, and switches to the non-root user. Now look at the last line. The README said npm start, but this image has no npm. It already runs node, so the command is just the file name.

[02:12] Build it

Time to build it, and name it containerize node, one point oh point oh. On this machine, stage one comes from the cache. These exact dependencies were installed here before. Your first build runs the install. Stage two pulls the distroless image, and copies in only what it needs. And the build ends by naming your image.

[02:37] Run and review

Now run it in the background, and map a port. The right side is the app's port, three thousand. The left side is your machine's. Here we used eighteen nine eight five. Any free port works, just like the lesson's tip for when three thousand is taken. Ask the health endpoint. Two hundred okay, and status okay. Now review it like a teammate would. The user is non-root. And the image is a little over two hundred megabytes, most of it Node itself. Then stop it. We started it with the remove flag, so the container is gone too.

[03:16] Check yourself

Here's a question to check yourself. The health check says connection reset by peer, but the container is still running. What's the likeliest cause? The app is listening on the wrong address, or the wrong port, inside the container. Bind every address, and match the right side of the port mapping. Keep your folder and your Dockerfile. In the lab, you'll rebuild this image for ComputeSphere, and deploy it.

You need Docker running (set up in lesson 3.1.1), Git, and a terminal. You don't need Node, Python or Go installed: the build stage brings its own.

Pick one of the three samples, ideally not the language you know best, and close the recipe tabs. You'll work from the app's README and the checklist in your head. The hints are there if you get stuck.

  1. Get the samples

    git clone https://github.com/computesphere-samples/learn.git
    cd learn/labs
    ls -d containerize-*
    

    Already cloned it? Run git pull in learn instead. Then cd into the sample you picked.

    You should seeThree folders: containerize-go, containerize-node, containerize-python.

  2. Read the README

    Before writing anything, find three facts: which port it listens on, where its health endpoint is, and how it's started. Every Dockerfile choice follows from them.

    You should seeYou can name the app's port, its health path and the command that starts it.

  3. Write the .dockerignore

    List what must never reach the build: .git, any .env file, and whatever your machine builds locally (installed packages, a virtual environment, a compiled binary).

    You should seeA .dockerignore next to the app's files.

  4. Write the Dockerfile

    Two stages. The first installs dependencies or compiles. The second copies in only what the app needs to run, switches to a non-root user, sets PORT, and starts the app.

    You should seeA Dockerfile with two FROM lines and a USER line.

  5. Build it

    docker build -t containerize-<language>:1.0.0 .
    

    A failed step prints the command that failed and its error. Fix the Dockerfile and build again; the steps before it come from the cache.

    You should seeThe build finishes and names your image, such as containerize-go:1.0.0.

  6. Run it and call /healthz

    docker run -d --rm --name my-app -p 3000:3000 containerize-<language>:1.0.0
    curl -i localhost:3000/healthz
    

    Check yourself

    curl says Connection reset by peer, but docker ps shows the container running. What's the likeliest cause?

    You should seeHTTP/1.1 200 OK and {"status":"ok"}.

  7. Review it like a teammate would

    docker image inspect containerize-<language>:1.0.0 --format '{{.Config.User}}'
    docker image ls containerize-<language>
    docker stop my-app
    

    Compare with the recipe now: about 15 MB for Go, a little over 200 MB for Node and Python. Keep your folder and Dockerfile: the lab rebuilds the image for ComputeSphere and deploys it.

    You should seeA user that isn't root or empty, and a size you can explain.

Hints, by language
  • Node: node:22-slim to run npm ci --omit=dev, then gcr.io/distroless/nodejs22-debian12:nonroot. Copy node_modules and server.js; CMD ["server.js"]. The README says npm start, but distroless has no npm: it already runs node, so CMD ["server.js"] means node server.js, and CMD ["npm", "start"] fails with Cannot find module '/app/npm'.
  • Python: python:3.13-slim for both stages. Install into a virtual environment in /opt/venv, copy it across, add a user with useradd, and start gunicorn bound to 0.0.0.0:${PORT}.
  • Go: golang:1.27-alpine to build with CGO_ENABLED=0, then gcr.io/distroless/static-debian12:nonroot with only the binary.
If it doesn't work
  • The container exits at once: run it without --rm, then docker logs my-app. A missing file means the runtime stage didn't copy it.
  • Permission denied: the non-root user can't read or write a path. Distroless images have no shell, so fix it in the build stage or with COPY --chown.
  • Port already in use: something else has 3000 on your machine. Use -p 3001:3000 and curl port 3001.