A container packages your application and everything it needs to run — code, runtime, libraries, environment variables — into a single portable unit. Run it on your laptop, a CI server, or a cloud VM, and it behaves identically in all three.
The most common containerisation tool is Docker. When developers say "container," they almost always mean a Docker container.
If Cartara flagged this in your diff, you likely added or modified a Dockerfile, docker-compose.yml, or a container-related deployment config.
The Problem Containers Solve
The classic complaint: "It works on my machine."
This happens because software depends on its environment — the specific version of Node.js installed, the system libraries available, the OS. When those differ between development, CI, and production, things break in hard-to-reproduce ways.
Containers solve this by making the environment part of the application. If it runs in the container on your machine, it runs in the container in production.
Core Concepts
Image
A read-only template that defines a container. Built from a Dockerfile. An image contains the OS layer, runtime, dependencies, and application code.
Think of an image as a recipe. You build it once; you can create many containers from it.
Container
A running instance of an image. Lightweight, isolated, and ephemeral. You can run multiple containers from the same image simultaneously.
Dockerfile
A text file with instructions for building an image:
# Start from an official Node.js image
FROM node:20-alpine
# Set the working directory inside the container
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy application code
COPY . .
# Expose the port the app runs on
EXPOSE 3000
# Command to start the app
CMD ["node", "server.js"]Docker Compose
A tool for running multi-container applications locally. A docker-compose.yml file defines all the services your app needs (app server, database, Redis) and how they connect — so your entire local environment starts with one command:
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
depends_on:
- db
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:docker compose up starts all services. docker compose down stops them. This replaces "install Postgres locally, configure it, remember which port it's on" for every developer on your team.
What You'll See in Your Code
Container-related files in a diff:
Dockerfile— instructions for building the app's container imagedocker-compose.yml— defines local services (app, database, Redis, worker).dockerignore— lists files to exclude from the image (like.env,node_modules,__tests__)railway.toml,fly.toml,render.yaml— platform-specific deployment configs that often reference Docker
A typical .dockerignore:
node_modules
.env
.env.*
__tests__
*.test.ts
.gitContainers vs. Serverless
| Containers | Serverless | |
|---|---|---|
| Startup time | Seconds | Milliseconds–seconds |
| Management | You manage the runtime | Fully managed |
| Scaling | Manual or auto | Automatic |
| Cost at low traffic | Always-on | Near zero (scales to zero) |
| Execution time | Unlimited | Limited (minutes) |
| Cold starts | No | Yes |
Use containers when: You have long-running services, need consistent latency (no cold starts), or have complex dependencies.
Use serverless when: Traffic is variable or low, tasks are short-lived, or you don't want to think about infrastructure.
Most small apps start serverless (Vercel, Netlify) and add containers later as specific needs arise.
Running Containers in Production
Managed container platforms handle the infrastructure for you — you just provide the image:
| Platform | Notes |
|---|---|
| Railway | Simplest deployment, great developer experience |
| Render | Similar to Railway, good free tier |
| Fly.io | More control, runs containers globally |
| Google Cloud Run | Serverless containers — scales to zero, pay per request |
| AWS App Runner | Similar to Cloud Run, AWS ecosystem |
Cloud Run is particularly worth knowing: it runs containers but bills like serverless — you pay only when requests are handled, and scale to zero when idle.
Best Practices
Keep images small. Use Alpine-based images (node:20-alpine) and add a .dockerignore to exclude unnecessary files. Smaller images build faster and deploy faster.
Don't store state in containers. Containers are ephemeral — they can be stopped and replaced at any time. Store files and persistent data in external storage (S3, a database, a volume).
Pass secrets as environment variables. Never bake API keys or database URLs into the image. Pass them at runtime. See Secrets and Config Management.
Pin base image versions. FROM node:20-alpine is more predictable than FROM node:alpine (which follows the latest version and can change unexpectedly).
Common Docker Commands
# Build an image from your Dockerfile
docker build -t myapp:latest .
# Run a container from the image
docker run -p 3000:3000 myapp:latest
# Start all services defined in docker-compose.yml
docker compose up
# Start in the background
docker compose up -d
# View running containers
docker ps
# View logs from a container
docker logs <container_id>
# Open a shell inside a container (for debugging)
docker exec -it <container_id> sh
# Stop all services
docker compose down