Step 1: Write the backend Dockerfile
You planned the sequence. Backend first because the frontend depends on the API. Now write the Dockerfile.
Direct Claude with the curated context you prepared -- the backend's package.json, server.js, .env.example, and the Docker guide. Tell Claude to write a Dockerfile for the Express backend. But be specific about the constraints:
- Use
node:20-slimas the base image -- notnode:latest, notnode:20. Pinned, slim. - Run the application as a non-root user. Not root.
- Use
npm cito install dependencies, notnpm install.npm ciis reproducible -- it installs exactly what's inpackage-lock.json. - Create a
.dockerignoreto exclude.git,node_modules,.env, and log files from the build context.
These constraints matter. AI commonly generates Dockerfiles with floating tags, root users, and no .dockerignore. Each of those defaults works today. Each one creates a problem you won't see until something breaks in production.
The Dockerfile you get is a document. Anyone reading it should understand exactly what the application needs: which runtime, which dependencies, how they're installed, what port it listens on, how it starts. If the Dockerfile is unclear, the environment is unclear.
Step 2: Build the image
Run the build:
docker build -t himalaya-backend .
Watch the output. Each step corresponds to a Dockerfile instruction. Docker prints what it's doing at each layer -- pulling the base image, copying files, installing dependencies.
Notice which layers take time (installing dependencies) and which are instant (copying files). On the second build, Docker caches the layers that haven't changed. If you only changed your application code, the dependency layer doesn't rebuild. This is why the instruction order matters -- put the slow, stable layers first.
These concepts -- images, layers, build arguments, port mapping -- work the same in Docker, Podman, and any OCI-compatible runtime. Docker is one implementation of a standard. The student who understands why layers exist and what a base image provides can work with any container runtime.
Step 3: Run the container
Start the container:
docker run -p 3000:3000 -e DATABASE_URL=postgres://username:password@host.docker.internal:5432/himalaya_treks himalaya-backend
The -p 3000:3000 maps port 3000 on your machine to port 3000 inside the container. The -e flag passes the database URL as an environment variable. host.docker.internal lets the container reach the PostgreSQL database running on your host machine.
Test the API:
curl http://localhost:3000/api/health
You should get a JSON response with a status and timestamp. Try the treks endpoint:
curl http://localhost:3000/api/treks
The backend is now running inside a container. The same image could run on Pemba's server, on a staging machine, or on a different developer's laptop. The environment is captured in the image -- not in someone's head.
Step 4: Catch the AI defaults
Review the Dockerfile Claude generated. Cross-model review is especially important here because AI has consistent blind spots with infrastructure code.
Check these specific things:
docker exec $(docker ps -q --filter ancestor=himalaya-backend) whoami
Is the container running as root? If it is, the Dockerfile is missing a USER instruction. Running as root inside a container grants unnecessary privileges -- a security anti-pattern.
docker image ls himalaya-backend
What's the image size? With node:20-slim, a Node.js backend should be roughly 200-400MB. If it's over 1GB, something went wrong -- likely using node:20 (full image) or copying node_modules into the image.
Check the Dockerfile itself. Is the base image pinned to a version (node:20-slim)? Or is it node:latest? Does a .dockerignore file exist? Open it -- does it exclude .git, node_modules, .env?
If any of these defaults slipped through, fix them. Direct Claude to correct the Dockerfile with the specific constraint that was violated.
Check: Run docker exec to check which user the container runs as. Is it root or a non-root user? Run docker image ls to check the image size. Can you explain why each layer exists?