Skip to content

Container Operations

Prerequisites

  • /etc/docker/daemon.json created with UT-approved network configuration
  • Docker installed (docker-ce, docker-ce-cli, containerd.io) and daemon active

Procedure: Install Docker Safely

When to use: Setting up Docker on a UT cloud VM for the first time.

Steps:

  1. Create the daemon configuration before installing or starting Docker:

    sudo mkdir -p /etc/docker
    sudo tee /etc/docker/daemon.json <<'EOF'
    {
      "bip": "192.168.67.1/24",
      "fixed-cidr": "192.168.67.0/24",
      "storage-driver": "overlay2",
      "mtu": 1400,
      "default-address-pools": [
        { "base": "192.168.167.1/24", "size": 24 },
        { "base": "192.168.168.1/24", "size": 24 },
        { "base": "192.168.169.1/24", "size": 24 },
        { "base": "192.168.170.1/24", "size": 24 },
        { "base": "192.168.171.1/24", "size": 24 },
        { "base": "192.168.172.1/24", "size": 24 },
        { "base": "192.168.173.1/24", "size": 24 },
        { "base": "192.168.174.1/24", "size": 24 }
      ]
    }
    EOF
    

  2. Add the Docker repository and install packages:

    sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    sudo dnf install docker-ce docker-ce-cli containerd.io
    

  3. Start and enable Docker:

    sudo systemctl enable --now docker
    

  4. Verify the network configuration is correct:

    ip -4 addr show docker0
    # Should show 192.168.67.1/24
    

  5. Run a test container:

    sudo docker run --rm registry.hpc.ut.ee/mirror/library/hello-world
    
    Expected output contains: Hello from Docker!

Troubleshooting:

  • If docker0 shows an unexpected IP (e.g., 172.17.0.1): stop Docker, fix daemon.json, then run sudo systemctl restart docker.
  • If the VM loses SSH connectivity after starting Docker without daemon.json: you must access the VM via ETAIS console.

Procedure: Pull and Run a Container

When to use: Launching a new application instance.

Steps:

  1. Pull image:

    docker pull registry.hpc.ut.ee/mirror/library/nginx:latest
    

  2. Run container (background mode):

    docker run -d --name my-nginx -p 8080:80 nginx:latest
    

  3. Verify:

    docker ps
    

Troubleshooting:

  • "Bind for 0.0.0.0:8080 failed: port is already allocated": Choose a different host port (e.g., -p 8081:80).

Procedure: Build an Image from a Dockerfile

When to use: Creating a custom image with your application code.

Steps:

  1. Create Dockerfile:

    FROM registry.hpc.ut.ee/mirror/library/python:3.9
    COPY . /app
    WORKDIR /app
    RUN pip install -r requirements.txt
    CMD ["python", "app.py"]
    

  2. Build image:

    docker build -t myapp:v1 .
    

Troubleshooting:

  • "COPY failed": Ensure source files exist in the build context (directory where you run build).

Procedure: Inspect a Running Container

When to use: Debugging configuration or networking issues.

Steps:

  1. View JSON metadata:

    docker inspect my-nginx
    

  2. Filter specific info (e.g., IP address):

    docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' my-nginx
    

Troubleshooting:

  • "No such object": Check container name with docker ps -a.

Procedure: View Container Logs

When to use: Application is crashing or behaving incorrectly.

Steps:

  1. View all logs:

    docker logs my-nginx
    

  2. Follow logs in real-time:

    docker logs -f my-nginx
    

Troubleshooting:

  • Logs are empty: Ensure application writes to stdout/stderr, not a file.

Procedure: Execute a Command Inside a Container

When to use: Manual troubleshooting, checking files, or database administration inside a container.

Steps:

  1. Open a shell:

    docker exec -it my-nginx /bin/bash
    
    (Use /bin/sh if bash is not available, e.g., Alpine images)

  2. Run a single command:

    docker exec my-nginx cat /etc/nginx/nginx.conf
    

Troubleshooting:

  • "exec failed": The container might be stopped or crashing.

Procedure: Publish a Container Port

When to use: Exposing an internal service to the host network.

Steps:

  1. Map port at runtime:

    docker run -p <host_port>:<container_port> <image>
    

  2. Example (Host 8080 -> Container 80):

    docker run -p 8080:80 nginx
    

Troubleshooting:

  • Service unreachable: Check host firewall and ensure container is listening on 0.0.0.0, not 127.0.0.1.

Procedure: Run a Container with a Named Volume

When to use: Deploying a service that needs persistent storage managed by Docker (databases, object stores, etc.).

Steps:

  1. Create the named volume:

    sudo docker volume create myservice-data
    

  2. Run the container mounting the volume:

    sudo docker run -d \
      --name myservice \
      --restart unless-stopped \
      -v myservice-data:/var/lib/myservice \
      myservice-image
    
    The volume data lives at /var/lib/docker/volumes/myservice-data/_data on the host.

  3. Inspect the volume:

    sudo docker volume inspect myservice-data
    sudo docker volume ls
    

Troubleshooting:

  • If data is missing after container recreation: confirm you used the same volume name and mount path.
  • Named volumes are not deleted by docker rm; use docker volume rm myservice-data to delete them explicitly.

Procedure: Keep a Container Running on Boot

When to use: Making a container survive system reboots or process crashes without wrapping it in a systemd unit.

Steps:

  1. Set a restart policy at run time:

    sudo docker run -d --name myapp --restart unless-stopped myimage
    
    unless-stopped restarts on crash and on host reboot, but stays stopped if you explicitly ran docker stop.

  2. Or update an existing container:

    sudo docker update --restart unless-stopped myapp
    

  3. Verify the policy was applied:

    sudo docker inspect myapp | grep -A1 RestartPolicy
    
    Expected: "Name": "unless-stopped"

  4. Confirm Docker itself starts on boot (it should already be enabled):

    sudo systemctl is-enabled docker
    

Troubleshooting:

  • Container does not start after reboot: check docker ps -a for exit codes and docker logs myapp for errors.
  • docker inspect shows "Name": "no": the restart policy was not set. Run docker update again.

Procedure: Containerise an Existing Service

When to use: Replacing a process managed by systemd with an equivalent Docker container.

Steps:

  1. Stop and disable the existing systemd service:

    sudo systemctl stop myservice
    sudo systemctl disable myservice
    

  2. Write a Dockerfile for the application. A Dockerfile needs to:

    • Choose a suitable base image (FROM)
    • Set a working directory inside the image (WORKDIR)
    • Copy application files and any dependency manifest into the image (COPY)
    • Install dependencies at build time (RUN)
    • Document the port the app listens on (EXPOSE)
    • Set the command that starts the application (CMD)

    Check the existing systemd unit file (ExecStart= line) for the exact command and any CLI arguments — include those in CMD.

    See Technologies: Docker — Dockerfile Reference for a full explanation of each instruction.

  3. Build the image (run from the directory containing the Dockerfile):

    sudo docker build -t myapp:latest .
    

  4. Run the container, bind-mounting any host paths the application must read/write:

    sudo docker run -d \
      --name myapp \
      --restart unless-stopped \
      -p 127.0.0.1:5000:5000 \
      -v /data/myapp:/data/myapp \
      myapp:latest
    
    Binding to 127.0.0.1:5000 ensures the port is only accessible locally (via a reverse proxy), not directly from the network.

  5. Verify the application responds:

    curl http://127.0.0.1:5000/
    

Troubleshooting:

  • Container exits immediately: check sudo docker logs myapp for the error. Common causes: missing files in the image, wrong CMD, or a port already in use.
  • Old systemd service still running and occupying the port: systemctl stop it before starting the container.

Procedure: Back Up Files to S3 with Restic

When to use: Creating encrypted, deduplicated backups of host files into an S3-compatible bucket (e.g., MinIO).

Steps:

  1. Install restic:

    sudo dnf install restic
    

  2. Set up environment variables (put these in a script or ~root/.bashrc to avoid retyping):

    export AWS_ACCESS_KEY_ID="<minio-root-user>"
    export AWS_SECRET_ACCESS_KEY="<minio-root-password>"
    export RESTIC_REPOSITORY="s3:http://127.0.0.1:9000/inventory-backup"
    export RESTIC_PASSWORD="<backup-encryption-password>"
    
    RESTIC_PASSWORD encrypts the backup data — do not lose it, or you cannot restore.

  3. Initialise the restic repository (one time only):

    sudo -E restic init
    
    Expected output contains: created restic repository

  4. Run the first backup:

    sudo -E restic backup /data/inventory
    

  5. Verify snapshots:

    sudo -E restic snapshots
    

  6. Optional — automate with cron (runs daily at 02:00):

    sudo crontab -e
    # Add:
    0 2 * * * AWS_ACCESS_KEY_ID=<user> AWS_SECRET_ACCESS_KEY=<pass> RESTIC_REPOSITORY=s3:http://127.0.0.1:9000/inventory-backup RESTIC_PASSWORD=<pass> restic backup /data/inventory
    

Troubleshooting:

  • Fatal: unable to open config file: Stat: ...: the repository has not been initialised. Run restic init first.
  • Fatal: wrong password or no key found: RESTIC_PASSWORD does not match the password used during restic init.
  • Connection refused: MinIO is not running. Check sudo docker ps.

Procedure: Audit an Image's Layers with Dive

When to use: Manually inspecting a third-party image before deploying it. Trivy can find known CVEs but cannot detect custom-planted malicious files — Dive lets you walk every filesystem change a layer introduces.

Steps:

  1. Make sure the image is in the local cache:
    sudo docker pull <image>
    
  2. Open the image in Dive:

    sudo dive <image>
    

  3. Walk the layer list (left panel) top-to-bottom. For each layer, read the Command: line — that's the Dockerfile instruction that produced it. Use Tab to switch to the file tree (right panel) and Ctrl+U to hide files unmodified in the selected layer. The remaining files are exactly what that layer added or changed.

  4. When you find a suspicious file (unexpected scripts in /usr/local/bin/, /opt/, /etc/cron.d/, …), note the layer's Digest: field. The digest is the SHA256 content hash of the layer and uniquely identifies it across pulls. Record it if you need to reference the layer later.

  5. Quit Dive with Q or Ctrl+C.

Troubleshooting:

  • unable to get image manifest: pull the image first.
  • permission denied while trying to connect to the Docker daemon socket: run with sudo.
  • Terminal looks broken / wrapped: resize the window. Dive is dense and needs ~120 columns.

Procedure: Scan an Image for Vulnerabilities with Trivy

When to use: Automated CVE scan of an image before deployment, alongside manual audit. Catches known vulnerabilities in OS packages and language dependencies.

Steps:

  1. Make sure Trivy is installed (see Technologies: Trivy — Installation).

  2. Run a CRITICAL-only scan against the image:

    trivy image --severity CRITICAL --no-progress <image>
    
    First run downloads the vulnerability database (~50–100 MB). Subsequent scans hit the cache.

  3. Read the table column-by-column:

    • Library — the affected package
    • Vulnerability — the CVE-ID (record this if you need to file or track it)
    • Statusfixed means a patched upstream version exists; affected means it's unpatched
    • Installed / Fixed Version — what's in your image vs. what fixes it
  4. For each CRITICAL CVE, decide: rebuild on a newer base image, upgrade the offending package, or accept the risk and document why.

  5. (Optional) If you need a clean machine-readable list of CVE-IDs:

    trivy image --severity CRITICAL --scanners vuln --format json <image> \
      | jq -r '.Results[].Vulnerabilities[]?.VulnerabilityID' | sort -u
    

Troubleshooting:

  • unable to download db: check that ghcr.io is reachable from the VM.
  • OS version is no longer supported by the distribution: expected on deliberately old base images. Trivy still reports CVEs; the warning just means upstream stopped patching.
  • Lots of HIGH/MEDIUM noise: filter with --severity CRITICAL or --severity CRITICAL,HIGH.

Procedure: Migrate Containers from docker run to Docker Compose

When to use: Consolidating multiple existing docker run-managed containers into a single declarative docker-compose.yml, without losing data in named volumes or breaking existing reverse-proxy setups.

Prerequisites: docker-compose-plugin installed (see Technologies: Docker Compose — Installation).

Steps:

  1. Inventory the existing containers. For each one, note the image, port mappings, volume mounts (named volumes vs bind mounts), environment variables, and restart policy:

    sudo docker inspect <container>
    

  2. Create a project directory and write docker-compose.yml describing every service. The translation from docker run flags to Compose keys:

    docker run flag Compose key
    --name foo service key (foo:)
    -p 8080:80 ports: ["8080:80"]
    -v data:/var/lib/foo (named volume) volumes: [data:/var/lib/foo] + top-level volumes: data:
    -v /host/path:/container/path (bind) volumes: [/host/path:/container/path]
    -e KEY=VALUE environment: { KEY: VALUE }
    --restart unless-stopped restart: unless-stopped
    --network mynet networks: [mynet] + top-level networks: mynet:
  3. Crucially, mark pre-existing named volumes and networks as external: true so Compose reuses them instead of creating empty new ones:

    volumes:
      minio-data:
        external: true
    services:
      minio:
        image: registry.hpc.ut.ee/mirror/minio/minio:latest
        volumes:
          - minio-data:/data
    
    If you skip this, Compose creates a brand-new volume with a project-prefixed name (e.g. lab10_minio-data) and your old data is silently orphaned.

  4. Validate the file before doing anything destructive:

    sudo docker compose config
    
    This parses the YAML, interpolates env vars, and prints the merged result. Any error here means the file would not start.

  5. Stop and remove the old docker run-managed containers (named volumes survive docker rm):

    sudo docker stop <name1> <name2> ...
    sudo docker rm <name1> <name2> ...
    

  6. Bring the stack up with Compose:

    sudo docker compose up -d
    

  7. Verify each service is running and Compose-managed:

    sudo docker compose ps
    sudo docker inspect <container> | grep com.docker.compose.project
    
    Compose-managed containers always have com.docker.compose.project=<project name> in their labels.

Troubleshooting:

  • Existing data missing after docker compose up: the named volume was not declared external: true. Fix the YAML, docker compose down, then docker compose up -d. The original volume should still be present (sudo docker volume ls) and intact.
  • Reverse proxy can no longer reach the service: confirm the new Compose container binds to the same host port and address as the old one. Compose does not automatically inherit the bind address from the previous container.
  • external network not found: the network was removed when you cleaned up. Recreate it manually (docker network create <name>) or remove external: true and let Compose create it.

Quick Reference

Action Command
Run (detached, restart) sudo docker run -d --restart unless-stopped image
List Running sudo docker ps
List All sudo docker ps -a
Logs sudo docker logs <name>
Follow Logs sudo docker logs -f <name>
Shell sudo docker exec -it <name> sh
Stop sudo docker stop <name>
Remove sudo docker rm <name>
Build sudo docker build -t name .
Named Volume Create sudo docker volume create vol-name
Named Volume List sudo docker volume ls
Set Restart Policy sudo docker update --restart unless-stopped <name>
Prune sudo docker system prune
Audit image layers sudo dive <image>
Scan image for CVEs trivy image --severity CRITICAL <image>
Compose stack up sudo docker compose up -d
Compose stack down sudo docker compose down
Compose validate sudo docker compose config