Here’s a practical, step-by-step guide to containerizing an app with Docker. I’ll show a simple example for a Node.js app, but the same workflow applies to many languages (Python, Go, etc.) with small tweaks to the Dockerfile.
High-level steps
- Install Docker on your machine
- Prepare your app sources
- Create a .dockerignore to keep the image small
- Write a Dockerfile (one or more stages)
- Build the Docker image
- Run a container from the image (and test locally)
- Iterate (add environment, volumes, or multi-stage builds)
- Push to a registry (optional)
Step-by-step
- Install Docker
- Windows/macOS: install Docker Desktop from Docker’s website and start it.
- Linux: follow distribution-specific instructions (often docker-ce package and adding your user to the docker group).
- Verify: run
docker version
anddocker info
.
- Prepare your app
- Ensure your app has a start script or entry point.
- Example for Node.js:
- package.json with a start script, e.g. “start”: “node index.js”
- index.js (or app.js) to start your server (e.g., Express).
- Create a .dockerignore
- This keeps the image small by excluding files not needed at runtime.
- Create a file named
.dockerignore
with content like:- node_modules
- npm-debug.log
- .git
- dist or build (if you don’t need prebuilt artifacts)
- .DS_Store
- coverage
- This helps lazy-install dependencies and avoid shipping dev files.
- Write a Dockerfile
- Create a file named
Dockerfile
in the project root. - Example for a Node.js app (multi-line for clarity):
- If you’re using a single-stage build (works for small apps):
- FROM node:18-alpine
- WORKDIR /app
- COPY package*.json ./
- RUN npm install –production
- COPY . .
- EXPOSE 3000
- CMD [“npm”, “start”]
- For a more robust, production-ready approach (multi-stage to reduce image size):
- Stage 1: builder
- FROM node:18-alpine AS builder
- WORKDIR /app
- COPY package*.json ./
- RUN npm install
- COPY . .
- RUN npm run build (if you have a build step)
- Stage 2: runtime
- FROM node:18-alpine
- WORKDIR /app
- COPY –from=builder /app /app
- RUN npm prune –production
- EXPOSE 3000
- CMD [“node”, “index.js”]
- Stage 1: builder
- If you’re using a single-stage build (works for small apps):
- Key points:
- Use a small base image (e.g., node:18-alpine) to reduce size.
- Copy package.json first to leverage Docker cache for dependencies.
- If you have environment-specific config, consider environment variables with placeholders.
- Build the Docker image
- Run in the project root (where Dockerfile is):
docker build -t myapp:1.0 .
- The tag
myapp:1.0
names the image; you can use any name/version.
- Run the container locally
- Basic run (map port 3000 to host 3000):
docker run -p 3000:3000 --name myapp-container myapp:1.0
- If your app needs env vars:
docker run -p 3000:3000 -e NODE_ENV=production --name myapp-container myapp:1.0
- If you want to keep logs/data outside container (volumes):
docker run -p 3000:3000 -v $(pwd)/data:/app/data --name myapp-container myapp:1.0
- Test your app
- Open http://localhost:3000 (or whatever port you exposed) in a browser or use curl.
- Check logs with:
docker logs myapp-container
- Attach to a running container (for interactive debugging):
docker attach myapp-container
(ordocker logs -f
for live logs)
- Iterate and improve
- Development workflow: use a volume for code changes to reflect without rebuilding:
docker run -p 3000:3000 -v "$PWD":/app -w /app --name myapp-container myapp:1.0
- Production considerations:
- Use a multi-stage Dockerfile to minimize image size.
- Use a non-root user inside the container.
- Pin exact base image versions to avoid surprises.
- Add HEALTHCHECK in Dockerfile to monitor app health.
- Optional: use Docker Compose for multi-container apps
- If your app depends on services (e.g., a database), create a docker-compose.yml:
- version: “3.9”
- services:
- web: build: . ports: – “3000:3000” environment: – NODE_ENV=production depends_on: – db
- db: image: postgres:15-alpine environment: – POSTGRES_USER=user – POSTGRES_PASSWORD=pass volumes: – db-data:/var/lib/postgresql/data
- volumes:
- db-data:
- Run:
docker-compose up --build
- Stop:
docker-compose down
- Push to a registry (optional)
- Tag your image for a registry (Docker Hub, GitHub Container Registry, etc.):
docker tag myapp:1.0 myusername/myapp:1.0
- Push:
docker push myusername/myapp:1.0
- Pull on another machine:
docker pull myusername/myapp:1.0
- For private registries, login first:
docker login myregistry.example.com
Common pitfalls and tips
- Missing dependencies: ensure dependencies are installed (RUN npm install) and build runs as part of the image if needed.
- Large image sizes: use multi-stage builds and alpine-based images; prune dev dependencies.
- File permissions: avoid running as root; create a non-root user and switch (USER node or similar).
- Environment-specific configuration: prefer environment variables over hard-coded values; use a config file pattern that reads from env.
- Port conflicts: ensure the container port matches your Dockerfile EXPOSE and your run command maps to an available host port.
- CI/CD: consider embedding these steps in your CI pipeline so images are built and tested automatically.