Have you ever spent an afternoon setting up PHP, MySQL, and a web server on a new machine, only to find that production behaves differently? We have — and we hated it. Docker fixes exactly that: every developer, every server, every client gets the same identical environment. No more "it works on my machine".
What is a container and why it changes your day
A container is a lightweight, isolated package that includes everything needed to run an application: code, runtime, libraries, environment variables. Unlike a virtual machine, it doesn't emulate an entire OS — it shares the host's kernel. Result: less disk space, starts in seconds, consumes fewer resources.
At Meteora Web, we started using Docker in 2018 for a client project with a legacy PHP application. Before, development on Linux, Mac, and Windows was a nightmare. After Docker, setup became a single command: docker compose up. Onboarding time dropped by 70%.
Practical analogy: shipping containers
Before shipping containers, cargo was loaded piece by piece — inefficient, slow, damage-prone. Standard containers made it possible to move any freight uniformly. Docker does the same for software: it packages application and dependencies into a standard format that runs on any machine with Docker installed.
Images and containers: the critical difference
An image is an immutable template (a read-only filesystem) containing the application and its dependencies. A container is a runnable instance created from that image. You can have multiple containers from the same image, each isolated.
# Pull the official Nginx image
docker pull nginx:latest
# Start a container from that image
docker run -d -p 8080:80 --name my-nginx nginx:latest
# Check it's running
docker psHere, nginx:latest is the image. The created container has its own filesystem, network, and processes, but shares the kernel. Open http://localhost:8080 and you'll see Nginx's default page.
The registry: where to find and share images
Registries are image repositories. The most well-known is Docker Hub, but alternatives like GitHub Container Registry, GitLab Container Registry, or self-hosted private registries exist. You can push your images and pull public ones.
Why use a registry? To avoid reinventing the wheel. Instead of manually configuring PHP, Apache, and MySQL each time, grab an official ready-made image. We do this for our clients: a base image with PHP 8.x, necessary extensions, preinstalled Composer — all in a single Dockerfile.
# Search images on Docker Hub from the command line
docker search ubuntu
# Pull a specific version
docker pull php:8.2-fpmWarning: don't use the latest tag in production. Images change, and you risk unexpected updates. Always specify an exact version, like nginx:1.27.0.
Essential commands to work with Docker
We use these commands every day at Meteora Web. Learn them and they'll become second nature.
Container management
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# Start a container in background (-d), map port (-p), name (--name)
docker run -d -p 8080:80 --name my-web nginx:1.27.0
# Stop a container
docker stop my-web
# Remove a container (must be stopped)
docker rm my-web
# Force remove while running
docker rm -f my-webImage management
# List local images
docker images
# Remove an image
docker rmi nginx:1.27.0
# Remove unused images (clean up space)
docker image prune -aExplore a running container
# Open an interactive shell inside the container
docker exec -it my-web /bin/bash
# View logs in real time
docker logs -f my-webFull practical example: want to test a Node.js app on a machine without Node installed? One command does it:
docker run -it --rm -v "$(pwd):/app" -w /app node:20-alpine node app.jsThe --rm flag removes the container when done, -v mounts the current directory, -w sets the working directory. Clean environment, no installation.
From code to image: the Dockerfile
When official images aren't enough, you create your own. The Dockerfile is a recipe that describes how to build an image. We write one for every client project — it guarantees that development, staging, and production environments are identical.
# Use an official base image
FROM nginx:alpine
# Copy app files into the server directory
COPY ./site /usr/share/nginx/html
# Expose port 80
EXPOSE 80
# Default command (already in the base image, but explicit)
CMD ["nginx", "-g", "daemon off;"]Now build the image and run it:
docker build -t my-site:v1 .
docker run -d -p 8080:80 my-site:v1Common Dockerfile mistakes:
- Copying everything (
COPY . .) without.dockerignore— includes node_modules, temp files, bloating the image. - Using
latestas base — better pin an exact version (e.g.nginx:1.27-alpine). - Running system updates in every build — use already updated base images.
Build and push: share your image
# Build with a tag that includes registry and username
docker build -t your-username/my-site:v1 .
# Log in to Docker Hub (or another registry)
docker login
# Upload the image to the registry
docker push your-username/my-site:v1From now on, anyone (or any server) can run docker pull your-username/my-site:v1 and get the exact same application. We use this to deploy updates to clients without manually configuring each server.
Optimization: smaller images, lower costs
One e-commerce client had Docker images of 2.1 GB for a Node.js app. Analyzing the layers, we found dev tools, npm cache, and unnecessary files. Using a multi-stage build and Alpine images, we reduced it to 120 MB — a 94% reduction. Across 10 servers, the space and bandwidth savings were huge.
# Multi-stage build: separate compilation from runtime
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]Security: don't leave ports open
Containers aren't inherently secure. If you run a container as root, an attacker might escalate privileges. At Meteora Web, we follow these rules:
- Use official, well-known images (Docker Official Images or verified ones).
- Run containers with a non-root user (
USERdirective in Dockerfile). - Don't expose unnecessary ports — map only what's needed.
- Update base images regularly: an outdated container is a vulnerability.
# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuserIn summary — what to do now
- Install Docker on your OS: follow the official guide at docs.docker.com.
- Run your first container:
docker run hello-world— verify everything works. - Get your hands on Nginx: pull the image (
docker pull nginx:alpine), start a container with a mapped port, modify the default page and reload. - Write a Dockerfile for an existing project (even a static HTML page) and build your own image.
- Explore Docker Hub to find useful images for your stack (PHP, Python, PostgreSQL).
From here on, everything becomes reproducible, portable, and measurable. Exactly how we like it.
Sponsored Protocol