Agentic AI with Java: Live Cohort
Claude CodeDeploying the Application on Google Cloud

Dockerizing and Testing the App Locally

Ask Claude Code for a production-ready Dockerfile with precise requirements, understand every line, then build and run the container locally with environment variables.

Writing a good Dockerfile by hand takes time, especially for a project with a slightly unusual layout. Claude Code can write it in seconds, if you tell it the details that matter. A vague prompt gives a generic Dockerfile that fails in subtle ways.

Prerequisite: Docker

Install Docker Desktop and make sure the Docker engine is running before you build. Check with:

docker --version

The prompt

This prompt includes every detail that caused problems in earlier attempts:

Prompt: Dockerfile
Write a Dockerfile at the project root for the Telusko Workflow Engine.

Project structure:
- Python 3.12 (pyproject.toml requires Python 3.12 or higher)
- Backend at backend/ with app/, seed.py, alembic/, alembic.ini, pyproject.toml
- Frontend at frontend/ (vanilla HTML/CSS/JS, no build step)
- Specs at specs/, REQUIRED at runtime because backend/seed.py reads
  Path(__file__).parent.parent / "specs" / "seed-data.json"
- backend/app/main.py is the real FastAPI app (backend/main.py is just a stub)
- backend/app/main.py mounts StaticFiles from directory "../frontend"
- alembic.ini lives in backend/; alembic upgrade head must run from backend/

Dockerfile requirements:
- Base image: python:3.12-slim
- Package manager: uv
- WORKDIR /app/backend so all relative paths in the code resolve correctly
- Optimise layer caching: copy pyproject.toml and install deps BEFORE
  copying application code
- Copy backend/app, backend/seed.py, backend/alembic, backend/alembic.ini
- Copy specs/ to ../specs (one level up from WORKDIR, required by seed.py)
- Copy frontend/ to ../frontend (one level up, served by StaticFiles)
- CMD: alembic upgrade head, then python seed.py, then uvicorn
- Bind to 0.0.0.0 on $PORT (default 8080; Cloud Run sets this env var)
- Add a comment on every line explaining what it does

Each requirement prevents a specific failure:

RequirementWithout it
Python 3.12Install fails because pyproject.toml requires 3.12+
specs/ copiedseed.py crashes because it cannot find seed-data.json
WORKDIR /app/backend and ../frontendThe static mount finds no files and every page is a 404
Bind 0.0.0.0The server only listens inside the container and is unreachable
$PORT, default 8080Cloud Run cannot reach the app. It sends traffic to the port in $PORT, 8080 by default
Dependencies before codeEvery code change reinstalls all packages, so builds are slow
Docker layer caching order

The Dockerfile Claude wrote

Dockerfile
# Use the official Python 3.12 slim image, matches requires-python >=3.12 in pyproject.toml
FROM python:3.12-slim

# Pull the uv binary from its official image; no pip install needed
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# All relative paths in the app (alembic.ini, ../frontend, ../specs) resolve from here
WORKDIR /app/backend

# Copy only the dependency manifest first so the next layer can be cached independently
COPY backend/pyproject.toml ./pyproject.toml

# Install runtime deps from pyproject.toml using stdlib tomllib (Python 3.11+)
# This layer is reused on every rebuild as long as pyproject.toml is unchanged
RUN python3 -c "\
import tomllib, subprocess; \
deps = tomllib.load(open('pyproject.toml', 'rb'))['project']['dependencies']; \
subprocess.run(['uv', 'pip', 'install', '--system'] + deps, check=True)"

# Copy the FastAPI application package
COPY backend/app ./app

# Copy Alembic migration scripts and config, required by `alembic upgrade head`
COPY backend/alembic ./alembic
COPY backend/alembic.ini ./alembic.ini

# Copy the idempotent seeder; run once at startup to populate demo tasks and users
COPY backend/seed.py ./seed.py

# Copy seed-data JSON one level up so seed.py finds ../specs/seed-data.json
COPY specs/ ../specs/

# Copy the static frontend one level up; main.py mounts it via StaticFiles("../frontend")
COPY frontend/ ../frontend/

# Document the default port; Cloud Run overrides this with the $PORT env var at runtime
EXPOSE 8080

# 1. Apply pending DB migrations  2. Seed demo data (idempotent)  3. Start the server
# sh -c is required so the shell expands ${PORT:-8080} at container start time
CMD ["sh", "-c", "alembic upgrade head && python seed.py && uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8080}"]

Possible improvements

This Dockerfile works and is easy to read. For a production team you might also:

  • Pin the uv image to a specific version tag instead of latest, so builds are reproducible (the uv Docker guide shows how)
  • Commit uv.lock (our .gitignore excludes it) and install with uv sync --frozen, so the container gets exactly the versions you tested
  • Add a .dockerignore file to keep .venv, .git, *.db and .env out of the build context
  • Run as a non-root user

Each of these is a good small prompt for Claude Code.

Build the image

From the project root:

docker build -t telusko-app .

Docker downloads the base image, installs dependencies and copies the app. The result is an image called telusko-app:latest. Write that name down, along with the other names you will need later:

NameValue in our example
Docker imagetelusko-app:latest
Google Cloud project IDtelusko-content-tracker (yours will differ)
Artifact Registry repositorytelusko-repo

Run the container

The app needs its JWT settings as environment variables. For a local test we use a throwaway secret:

docker run -p 8080:8080 \
  -e JWT_SECRET_KEY='local-test-secret-not-for-production' \
  -e JWT_ALGORITHM='HS256' \
  -e JWT_ACCESS_TOKEN_EXPIRE_MINUTES='60' \
  telusko-app
docker run -p 8080:8080 `
  -e JWT_SECRET_KEY='local-test-secret-not-for-production' `
  -e JWT_ALGORITHM='HS256' `
  -e JWT_ACCESS_TOKEN_EXPIRE_MINUTES='60' `
  telusko-app
PartMeaning
-p 8080:8080Map port 8080 on your machine to port 8080 in the container
-e NAME=valueSet an environment variable inside the container
telusko-appThe image to run

In the logs you see the migrations run, the seed script insert the demo data, and uvicorn start. Open http://localhost:8080, log in as admin / admin123, and check the board.

The local secret is deliberately weak and obvious. Never use it outside your own machine. For Cloud Run we generate a real one.

Common problems

Error or symptomCauseFix
Cannot connect to the Docker daemonDocker Desktop is not runningStart Docker Desktop and wait for "Engine running"
FileNotFoundError: ... seed-data.jsonspecs/ was not copied, or copied to the wrong placeCOPY specs/ ../specs/ with WORKDIR /app/backend
Directory '../frontend' does not existfrontend/ was not copied one level upCOPY frontend/ ../frontend/
Container starts but the browser shows nothingPort not published, or uvicorn bound to 127.0.0.1Use -p 8080:8080 and --host 0.0.0.0
port is already allocatedSomething else uses port 8080Stop it, or map another host port: -p 9090:8080
Login fails with 500JWT environment variables missingPass all three -e JWT_... variables

A .dockerignore file

Without it, Docker sends everything in the folder to the build, including your virtual environment, the local database and your .env:

.dockerignore
.git
.venv
**/__pycache__
*.db
*.db-*
.env
**/.env
.claude/worktrees

This makes builds faster and keeps secrets out of the image.

Stop the container

Find the container ID and stop it:

docker ps
docker stop <container-id>

The image is tested and ready to go to the cloud.

Official references


Written By: Shramik Masti

How is this guide?

Last updated on