Docker Beginner Tutorial: From Installation to Containerized Web Deployment
Docker has become the standard technology for modern software deployment. It packages applications and their dependencies into standardized containers, ensuring consistent operation across any environment.
Many developers know the classic story: a Node service runs perfectly on your laptop, then starts throwing "missing dependency" errors on the server; or a project that boots fine on a colleague's machine crashes on yours. These environment inconsistencies are exactly what Docker eliminates. By bundling the code, the runtime, and the system libraries into a read-only image, it removes the "works on my machine" debate at the source. This tutorial walks the full path from scratch — install, build an image, run a container, orchestrate with Compose, and deploy to a server — and every command is copy-paste ready.
1. Docker Core Concepts
1.1 What is a Container
Traditional Deployment:
Application → Dependencies → OS → Server
(Environment differences cause "it works on my machine" problems)
Container Deployment:
Docker Container (App + Dependencies) → Docker Engine → OS
(Standardized, consistent environment)
Containers are often compared with virtual machines, but their isolation layers are completely different. A VM virtualizes the entire hardware stack (hypervisor plus guest OS) and typically takes up several gigabytes; a container virtualizes only the operating system layer, sharing a single Linux kernel, starting in a few hundred milliseconds, and usually weighing in at tens to hundreds of megabytes. That's why a 4-core, 8GB cloud server can comfortably run a dozen containers but only a few VMs.
1.2 Key Terminology
| Term | Description | Analogy |
|---|---|---|
| Image | Read-only template of app and dependencies | Installation disc |
| Container | Running instance of an image | Running program |
| Dockerfile | Instructions for building an image | Recipe |
| Docker Hub | Image registry | App store |
| Docker Compose | Multi-container management tool | One-click launch |
To connect the dots: docker build "bakes" your project into an image following the Dockerfile's instructions, and docker run starts a container from that image. If you break something inside a container, just delete it and start a new one — the image itself stays clean and unchanged. This "immutable image + disposable container" idea is the foundation of everything that follows, from orchestration to CI/CD.
2. Installing Docker
# macOS
brew install docker --cask
# Or visit https://www.docker.com/products/docker-desktop
# Ubuntu
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER # Run docker without sudo
# Verify installation
docker --version
docker run hello-world
After installing, run docker run hello-world once — if it prints the welcome message, the engine is working. Docker Desktop ships with a visual Dashboard where beginners can watch images, containers, and volumes at a glance; on a server you usually install just Docker Engine and drive it from the command line.
Reference: https://docs.docker.com/get-started/ (official getting-started guide) · https://docs.docker.com/engine/install/ (installation docs per platform)
3. Your First Docker Application
3.1 Create a Simple Web Application
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
// server.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello Docker!');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Every line in a Dockerfile is one "build instruction", and the order matters. Copying package*.json first and then running npm install exploits layer caching — as long as the dependency files don't change, that layer isn't rebuilt. If you copy everything with COPY . . first, any code change invalidates the whole cache and every build reinstalls dependencies, which can turn a few seconds into several minutes on a larger project.
3.2 Build and Run
# Build image
docker build -t my-web-app .
# Run container
docker run -d -p 3000:3000 --name my-app my-web-app
# List running containers
docker ps
# View logs
docker logs my-app
# Stop container
docker stop my-app
# Remove container
docker rm my-app
-p 3000:3000 maps the host's port 3000 to the container's port 3000 — host on the left of the colon, container on the right. Change it to -p 8080:3000 and you can visit via 8080. -d runs the container in the background, and --name gives it a friendly label for later commands. Open http://localhost:3000 in your browser and you should see "Hello Docker!".
4. Docker Compose: Multi-Service Management
4.1 What is Docker Compose
Docker Compose defines and runs multi-container applications using a single YAML file, ideal for scenarios involving web apps + databases + cache. Running one web container is fine with a single docker run, but once you add MySQL, Redis, and a queue worker, the commands grow long, hard to maintain, and easy to get the start order wrong. Compose captures the whole application topology in docker-compose.yml, and one docker-compose up -d brings everything up — the standard way to run classic multi-container setups like a LAMP environment.
4.2 docker-compose.yml Example
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DB_HOST=db
- DB_USER=app
- DB_PASSWORD=secret
depends_on:
- db
volumes:
- .:/app
- /app/node_modules
db:
image: mysql:8.0
environment:
- MYSQL_DATABASE=app
- MYSQL_USER=app
- MYSQL_PASSWORD=secret
- MYSQL_ROOT_PASSWORD=rootsecret
volumes:
- db_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
db_data:
Note that depends_on only controls start order, not readiness — MySQL being up doesn't mean it accepts connections yet. The robust approach is to add retry logic in the app, or use Compose healthchecks so the web service starts only after the database reports healthy. Under volumes, .:/app mounts your code directory into the container so edits don't require a rebuild, while db_data is a named volume that keeps database files alive even if the container is deleted.
4.3 Start Multi-Service
# Start all services
docker-compose up -d
# Check service status
docker-compose ps
# View logs
docker-compose logs -f web
# Stop all services
docker-compose down
down stops the containers but keeps the data volumes by default. If you want to wipe the data too, use docker-compose down -v — that command is irreversible, so think before you run it.
5. Deploy to Server
5.1 Deploy with Docker to VPS
# 1. Install Docker on the server
# 2. Transfer project files
scp -r ./my-app user@server:/var/www/
# 3. Run on the server
ssh user@server
cd /var/www/my-app
docker-compose up -d --build
5.2 Optimized Deployment with Docker
# Multi-stage build (reduce image size)
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Run (only include files needed for runtime)
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]
The core idea of multi-stage builds is "only carry what's needed to run into the final image". Static dist files produced by a frontend build can be served by a lightweight Nginx image — there's no reason to drag the entire Node build environment into the artifact. For an app like Next.js, multi-stage builds typically shrink the image from over 1GB to around 200MB, which makes pushing to a registry and pulling on deployment noticeably faster.
6. Common Docker Commands
# Image management
docker images # List local images
docker pull nginx # Pull an image
docker rmi <image_id> # Remove an image
# Container management
docker run -d nginx # Run in background
docker exec -it <id> /bin/bash # Enter a container
docker logs -f <id> # View logs
docker cp <id>:/path /local # Copy files
# Cleanup
docker system prune # Clean unused containers and images
docker volume prune # Clean unused volumes
7. Best Practices
1. Image optimization:
- Use Alpine base images
- Multi-stage builds
- Combine RUN commands to reduce layers
2. Security:
- Do not run containers as root
- Regularly update base images
- Use .dockerignore to exclude sensitive files
3. Data persistence:
- Use volumes for stateful services like databases
- Use bind mounts for configuration files
4. Logging:
- Output container logs to stdout/stderr
- Use Docker's log drivers for collection
.dockerignore is easy to overlook but matters for both build speed and security: list node_modules, .git, *.log, and .env in your project root so they never end up in the image or the build context. That keeps secrets out of your images and makes COPY . . much faster.
8. Frequently Asked Questions
- My file changes inside a container disappear after restart? Containers are ephemeral — deleting removes them. Put data you need to keep in volumes, and let the image own the code.
- The port is already in use? Change the host-side mapping (e.g.
-p 8080:3000), or rundocker psto see which container holds the port. - The image keeps failing to pull? Check your network and registry mirror; on some networks configure a registry mirror, or switch to a different registry in the Dockerfile.
- The container keeps restarting? Read the startup logs with
docker logs— usually it's an environment variable, port, or dependency service issue.
9. Summary
Docker is an indispensable tool for modern application deployment. Mastering its core concepts lets you achieve "build once, run anywhere." Start with a simple single-container web app, gradually learn Docker Compose multi-service orchestration, and eventually build a complete CI/CD pipeline — paired with a well-chosen cloud server, the whole environment runs itself.
Reference: https://docs.docker.com/get-started/ (official getting-started guide)