L2 Pending

Containerizing Services with Docker

By Lint8590 Level 2

You’ve got a server running. You could install services directly on it, like Nginx or Postgres. But that gets messy. Different apps need different versions of the same dependency. Uninstalling leaves junk behind. And trying something new means risking what already works.

Docker fixes that. Each service runs in its own isolated container. Want to try something? Spin it up. Don’t like it? docker rm and it’s gone.

Before You Start

You need Docker installed. On Ubuntu Server:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Log out and back in for the group change to take effect. Then test it:

docker run hello-world

You should see a welcome message. Permission error means you forgot to log back in.

Method 1: Docker Compose

This is what you’ll use day-to-day. You define everything in a YAML file: what image, what ports, what folders to mount, what environment variables to set. One command and it all runs.

Your First Compose File

mkdir ~/docker-services
cd ~/docker-services

Create docker-compose.yml:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html
    restart: unless-stopped

Make a page to serve:

mkdir html
echo "<h1>My homelab is alive</h1>" > html/index.html

Start it:

docker compose up -d

Open http://your-server-ip in a browser and you should see the page.

The -d flag means detached, so it runs in the background. Without it, logs fill your terminal.

Adding More Services

Add a database to the same file:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: changeme
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  pgdata:

Run docker compose up -d again. Compose notices the new service and only starts Postgres. Nginx stays up because it didn’t change. You keep editing the file and re-running up -d.

Useful Compose Commands

docker compose up -d        # Start everything
docker compose down         # Stop and remove containers
docker compose logs -f      # Follow logs from all services
docker compose ps           # Show running services
docker compose restart      # Restart everything

Persistent Data

Databases need storage that survives container restarts. In Compose you use volumes. Two types:

  • Named volumes - Docker manages where the data lives. Good for databases.
  • Bind mounts - You point to a specific directory. Good for config files, HTML, stuff you want to edit directly.

Bind mount example:

services:
  my-app:
    image: my-app:latest
    volumes:
      - ./config:/app/config
      - ./data:/app/data

Tips

Keep everything in one docker-compose.yml at first. Split only when you have more than 5-6 services and it gets hard to find things.

Pin your image versions. nginx:latest will change under you. nginx:alpine won’t. Alpine variants are smaller - nginx:alpine is about 25MB vs 180MB for the full image.

Set restart: unless-stopped on services you want to keep running. Without it, Docker kills them on reboot.


Method 2: Plain Docker CLI

The CLI is faster for quick tests. No files to create. Just type and run.

Running a Container

docker run -d --name nginx -p 80:80 nginx:alpine

Breaking down the flags:

  • -d - detach (background)
  • --name nginx - name so you can reference it later
  • -p 80:80 - map port 80 on your server to port 80 in the container
  • nginx:alpine - the image

Check it:

docker ps

You should see the container with a status like “Up 2 minutes.”

Commands

docker ps                    # Running containers
docker ps -a                 # All containers (including stopped)
docker stop nginx            # Stop by name
docker rm nginx              # Remove a stopped container
docker logs nginx            # Show logs
docker logs -f nginx         # Follow logs
docker exec -it nginx sh     # Open a shell inside the container

Port Mapping

Multiple web services can’t all use port 80. Map them to different host ports:

docker run -d --name app1 -p 8080:80 nginx:alpine
docker run -d --name app2 -p 8081:80 nginx:alpine

app1 is at http://your-server:8080. app2 is at http://your-server:8081.

Mounting Files

Containers are temporary. Remove one and its files go with it. Mount a directory to keep data around:

docker run -d --name nginx -p 80:80 -v /path/on/host:/path/in/container nginx:alpine

Real example:

mkdir ~/nginx-html
echo "<h1>Hello from Docker</h1>" > ~/nginx-html/index.html
docker run -d --name nginx -p 80:80 -v ~/nginx-html:/usr/share/nginx/html nginx:alpine

Edit files in ~/nginx-html and refresh the browser. Changes show instantly.

Environment Variables

Many images need env vars for configuration:

docker run -d \
  --name postgres \
  -e POSTGRES_PASSWORD=changeme \
  -e POSTGRES_DB=myapp \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16-alpine

The -e flag sets environment variables. Check the image’s docs to know which ones are required.

Cleaning Up

Containers and images pile up. Clean occasionally:

docker container prune    # Remove all stopped containers
docker image prune        # Remove unused images
docker system prune       # Everything + networks, build cache

docker system prune is safe but aggressive. Skip it if you have stopped containers you might want to inspect.


Next Steps

You can run services. That’s the hard part. From here, look at Choosing Server Hardware if you want to upgrade,

Next Steps

Test Yourself