Docker has become the de‑facto standard for packaging applications, but its true power shines when you weave containers into a full‑blown DevOps workflow. In this guide we’ll walk through everything you need to spin up, secure, and orchestrate Docker containers as part of a continuous integration and delivery (CI/CD) pipeline. By the end, you’ll have a production‑ready setup, a toolbox of best‑practice commands, and a clear view of the traps that trip even seasoned engineers.
What You’ll Need
- A Linux workstation or VM (Ubuntu 22.04 LTS is recommended)
- Docker Engine 24.x or newer installed
- Docker Compose 2.x
- A Git repository for your application code
- Access to a CI/CD platform (GitHub Actions, GitLab CI, or Jenkins)
- Basic knowledge of Linux command line and YAML syntax
Step 1: Install and Verify Docker Engine
First, ensure Docker Engine is correctly installed and the daemon is running. Run the official convenience script or use the repository method. The repository method gives you more control over versions.
# Add Docker’s official GPG key
sudo apt-get update && sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Set up the stable repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Verify installation
docker version
If the docker version command returns client and server details without errors, you’re ready to move on.
Step 2: Create a Minimal Dockerfile for Your Application
Dockerfiles describe how to build a container image. Below is a generic example for a Node.js microservice. Adjust the base image, runtime, and build steps to match your stack.
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
Key points:
- Use multi‑stage builds to keep the final image lean.
- Pin the base image tag (e.g.,
node:20-alpine) to avoid unexpected upgrades. - Expose only the ports your service needs.
Step 3: Build, Tag, and Push the Image to a Registry
In a DevOps pipeline you’ll push the image to a registry (Docker Hub, GitHub Packages, or a private registry). The following commands illustrate a manual flow; later we’ll automate it.
# Build the image locally
docker build -t myorg/myservice:1.0.0 .
# Log in to the registry (Docker Hub example)
echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
# Push the image
docker push myorg/myservice:1.0.0
Notice the use of environment variables for credentials – never hard‑code secrets.
Step 4: Define a Docker‑Compose File for Local Development and Testing
Docker‑Compose lets you spin up multiple containers (app, database, cache) with a single command. This file mirrors the production stack but adds developer‑friendly options such as volume mounts.
# docker-compose.yml
version: "3.9"
services:
app:
image: myorg/myservice:1.0.0
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
volumes:
- .:/app:cached
depends_on:
- db
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass
POSTGRES_DB: devdb
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Run docker compose up --build to start the stack. Your code changes are instantly reflected thanks to the bind mount.
Step 5: Integrate Docker into a CI/CD Pipeline
Now we automate the build‑test‑deploy cycle. Below is a GitHub Actions workflow that builds the image, runs unit tests inside a container, and pushes the image on a successful run.
# .github/workflows/docker-ci.yml
name: Docker CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-test-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build image
id: build
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: myorg/myservice:${{ github.sha }}
load: true
- name: Run tests inside container
run: |
docker run --rm myorg/myservice:${{ github.sha }} npm test
- name: Push image to registry (only on main)
if: github.ref == 'refs/heads/main'
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myorg/myservice:latest, myorg/myservice:${{ github.sha }}
This workflow demonstrates three best practices:
- Build once, reuse the same image for testing and deployment.
- Never expose secrets; use GitHub Secrets.
- Tag images with both
latestand the commit SHA for traceability.
Step 6: Deploy to a Production Orchestrator (Docker Swarm or Kubernetes)
For most enterprises Kubernetes is the go‑to orchestrator, but Docker Swarm still offers a lightweight entry point. Below we show a minimal Docker Swarm stack file; the same image can later be promoted to a Helm chart for Kubernetes.
# stack.yml (Docker Swarm)
version: "3.8"
services:
web:
image: myorg/myservice:latest
ports:
- "80:3000"
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
environment:
- NODE_ENV=production
secrets:
- db_password
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: produser
POSTGRES_DB: proddb
volumes:
- db_data:/var/lib/postgresql/data
deploy:
placement:
constraints: [node.role == manager]
volumes:
db_data:
secrets:
db_password:
external: true
Initialize Swarm (if you haven’t already) and deploy:
docker swarm init
docker stack deploy -c stack.yml prod
Observe the rollout with docker service ls and docker service ps web. For Kubernetes, translate the above into a Deployment, Service, and Secret manifest, then apply with kubectl apply -f.
Step 7: Implement Continuous Monitoring and Logging
Containers are ephemeral; you need centralized logs and metrics. The ELK stack (Elastic + Logstash + Kibana) or the EFK stack (Fluentd instead of Logstash) pairs nicely with Docker.
# docker-compose.logging.yml (EFK example)
version: "3.9"
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
environment:
- discovery.type=single-node
ports:
- "9200:9200"
kibana:
image: docker.elastic.co/kibana/kibana:8.12.0
ports:
- "5601:5601"
fluentd:
image: fluent/fluentd:v1.16-debian-1
volumes:
- ./fluentd/conf:/fluentd/etc
ports:
- "24224:24224"
Configure your application containers to ship logs to Fluentd (or directly to Elasticsearch) by adding a --log-driver=fluentd flag in the compose file. Metrics can be scraped by Prometheus, and alerts sent via Alertmanager.
Common Mistakes to Avoid
Even seasoned engineers stumble over a few recurring pitfalls:
- Hard‑coding secrets in Dockerfiles. Use Docker secrets, environment variables injected at runtime, or a secret manager like HashiCorp Vault.
- Running containers as root. Add a non‑root user in the Dockerfile (
RUN adduser -D appuser) and switch to it withUSER appuser. - Neglecting image size. Unnecessary layers, large build‑time dependencies, or copying the entire repo increase attack surface and start‑up time.
- Skipping multi‑arch builds. If your pipeline runs on both x86_64 and ARM (e.g., Raspberry Pi edge nodes), enable
docker buildxwith--platform linux/amd64,linux/arm64. - Forgetting to prune unused images. Over time the host can run out of space; schedule
docker system prune -for use a CI job to clean up.
Tips and Tricks
Boost productivity and security with these advanced techniques:
- Layer caching. Order your Dockerfile so that rarely‑changing layers (e.g.,
npm ci) appear early. This speeds up rebuilds. - Build secrets. Use
--secret id=npmrc,src=$HOME/.npmrcduring build to keep credentials out of the final image. - Healthchecks. Add
HEALTHCHECK CMD curl --fail http://localhost:3000/health || exit 1to automatically restart unhealthy containers. - GitOps. Store your Docker Compose or Kubernetes manifests in a Git repo and let Argo CD or Flux reconcile the desired state.
- Resource limits. Define
mem_limitandcpusin compose orresources.limitsin Kubernetes to prevent noisy‑neighbor problems.
Frequently Asked Questions
Can I use Docker without root privileges?
Yes. Add your user to the docker group (sudo usermod -aG docker $USER) and log out/in. For production servers, consider rootless Docker, which runs the daemon as an unprivileged user and reduces the attack surface.
How do I handle database migrations in a containerized pipeline?
Treat migrations as a separate job in your CI/CD workflow. Build a lightweight image that contains only the migration scripts, run it against the target database, and only proceed to deployment if it exits with code 0. This keeps your application containers immutable.
What’s the difference between Docker Compose and Kubernetes?
Docker Compose is ideal for local development and small‑scale stacks; it defines services, networks, and volumes in a single file. Kubernetes is a production‑grade orchestrator that adds declarative state, automatic scaling, self‑healing, and a rich ecosystem of operators. You can start with Compose and later translate the same definitions into Helm charts for Kubernetes.
Conclusion
Docker containers are far more than isolated sandboxes—they are the building blocks of modern DevOps pipelines. By following the steps above—installing Docker, crafting efficient Dockerfiles, automating builds with CI, deploying to an orchestrator, and wiring in monitoring—you’ll achieve repeatable, fast, and secure releases. Keep an eye on common mistakes, leverage the tips, and continuously iterate on your pipeline. The result is a resilient delivery process that scales with your organization and lets you focus on delivering value, not on wrestling with infrastructure.
Photo by Rubaitul Azad on Unsplash






