Learn WebSocket - Docker & Containerization
Episode 27 of 34

Learn WebSocket - Docker & Containerization

This episode wraps the WebSocket server in a container: a Dockerfile with a multi-stage build, Node.js best practices, docker networking and port mapping, Docker Compose with Redis, and container health checks.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

A WebSocket server running directly on a machine is a time bomb: configuration depends on the machine, Node.js versions differ between environments, and dependencies can break. Containerization locks the whole environment into one image that runs anywhere.

Episode 27 covers Docker & containerization: writing a Dockerfile for the WebSocket server, using a multi-stage build, understanding docker networking, wiring up Redis and the server with Docker Compose, and adding health checks.

The Basic Dockerfile

The Node.js Image

Start with the official Node.js image and the right structure.

WebSocket server Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN npm install --omit=dev
COPY server.js ./
 
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/server.js ./server.js
EXPOSE 8080
USER node
CMD ["node", "server.js"]

node:22-alpine produces a small, light image. USER node runs the application as a non-root user — an important security practice. The first layer installs dependencies, the second only copies what is needed.

Multi-Stage Builds

FROM ... AS builder followed by COPY --from=builder forms a multi-stage build: the first stage installs dependencies, the second produces a lean final image. The production image carries no unnecessary build tools.

Docker Networking

Port Mapping

Containers have their own network namespace. Expose ports to the host with mapping.

Running the container with a port
docker run -d --name ws-server -p 8080:8080 ws-server:latest

-p 8080:8080 maps host port 8080 to container port 8080. Without mapping, the port inside the container is unreachable from outside.

Network Isolation

Use a dedicated network so containers communicate safely without exposing ports publicly.

Create a dedicated network
docker network create ws-net

With docker network create ws-net and connecting containers to that network, the server can reach Redis via the service name without mapping the Redis port to the host — only the WebSocket port is open to the outside.

Docker Compose

Multi-Container Setup

Docker Compose wires all services together in one file.

Docker Compose WebSocket + Redis
services:
  ws-server:
    build: .
    ports:
      - "8080:8080"
    environment:
      REDIS_URL: redis://redis:6379
    depends_on:
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://localhost:8080/healthz').then(r => process.exit(r.ok ? 0 : 1))"]
      interval: 10s
      timeout: 5s
      retries: 3
 
  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s

depends_on with condition: service_healthy ensures the server waits for Redis to be healthy before starting. The variable REDIS_URL: redis://redis:6379 uses the service name, not an IP — Compose's internal DNS handles it.

Running the Stack

Run the whole stack
docker compose up -d

docker compose up -d builds the image, creates the network, and runs all services. One command brings a full production environment to a local machine.

Health Checks

Why Health Checks Matter

A health check distinguishes "the container is alive" from "the application works". The /healthz endpoint from episode 15 is the basis of this.

JSHealth check in the application
http.createServer((req, res) => {
  if (req.url === "/healthz") {
    const sehat = wss.clients.size < 5000;
    res.writeHead(sehat ? 200 : 503);
    res.end(sehat ? "ok" : "penuh");
    return;
  }
  res.writeHead(404);
  res.end();
}).listen(8080);

wss.clients.size < 5000 makes the health check reflect real capacity: the server responds 503 when nearly full, so the orchestrator knows when to add instances.

Readiness vs Liveness

In Kubernetes (episode 28), these two probes differ: liveness checks whether the app is still alive (restart on failure), readiness checks whether it is ready to accept traffic (stop traffic on failure). In Compose, a single healthcheck is enough for most cases.

Best Practices

  • Do not run containers as root.
  • Install only production dependencies; build tools belong in the build stage.
  • Store configuration in environment variables, not in the image.
  • Limit resources with mem_limit and cpu_limit.
  • Log to stdout so Docker and orchestrators can collect it.

Closing

Episode 27 turned a fragile WebSocket server into a portable image: locked dependencies, an isolated environment, and a complete stack (server plus Redis) running with a single command.

Key takeaways:

  • Multi-stage builds produce lean, safe images.
  • Run containers as a non-root user.
  • Port mapping and dedicated networks control what is exposed outside.
  • Docker Compose wires the WebSocket server and Redis together.
  • Health checks ensure the app is truly healthy, not just alive.
  • Store configuration in environment variables, not in the image.

In the next episode we bring the stack to orchestration: Kubernetes deployment — Deployment, Service, Ingress with session affinity, HPA, and service mesh.

Learn WebSocket - Docker & Containerization | Learn WebSocket