---
name: docker-install
description: "Install and configure Docker Engine + Compose on Linux (Ubuntu/Debian). Covers sudo-via-pipe pitfalls, GPG key handling, repo setup, and post-install steps."
tags: [docker, devops, linux, ubuntu, install, compose]
triggers:
  - install docker
  - setup docker
  - docker engine
  - docker compose
  - container runtime
---

# Docker Installation & Configuration

Install Docker Engine + Docker Compose plugin on Linux (primarily Ubuntu/Debian).

## Prerequisites Check

```bash
cat /etc/os-release | head -5   # OS version
which docker 2>/dev/null        # already installed?
whoami && id                    # current user
```

## Standard Install (requires root/sudo)

### When sudo needs a password

Use `echo 'PASSWORD' | sudo -S COMMAND` pattern. See pitfalls below.

### Steps

1. **Install dependencies**
   ```bash
   sudo apt-get update -y
   sudo apt-get install -y ca-certificates curl gnupg
   ```

2. **Add Docker GPG key** — download first, then dearmor (do NOT pipe curl into gpg when using sudo-via-pipe)
   ```bash
   curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /tmp/docker.gpg
   sudo install -m 0755 -d /etc/apt/keyrings
   sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg
   sudo chmod a+r /etc/apt/keyrings/docker.gpg
   ```

3. **Add Docker apt repo** — write to temp file, then copy (tee consumes stdin)
   ```bash
   # Detect arch: amd64, arm64, etc.
   ARCH=$(dpkg --print-architecture)
   echo "deb [arch=$ARCH signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /tmp/docker.list
   sudo cp /tmp/docker.list /etc/apt/sources.list.d/docker.list
   ```

4. **Install Docker packages**
   ```bash
   sudo apt-get update -y
   sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
   ```

5. **Post-install setup**
   ```bash
   sudo usermod -aG docker $USER    # add user to docker group
   sudo systemctl start docker
   sudo systemctl enable docker
   ```

6. **Verify**
   ```bash
   docker --version
   docker compose version
   docker info | head -5
   ```

## Rootless Docker (no root needed)

For environments without sudo access. Requires `/etc/subuid` and `/etc/subgid` entries for the user.

```bash
# Check prerequisites
cat /etc/subuid | grep $(whoami)
cat /etc/subgid | grep $(whoami)
# If missing, ask admin to add: username:100000:65536

dockerd-rootless-setuptool.sh install
```

### P8: "repository does not exist or may require docker login"
If `docker pull` returns this error after proxy is confirmed working, the repo genuinely doesn't exist or is private. Verify:
```bash
curl -x http://127.0.0.1:7890 -s "https://hub.docker.com/v2/repositories/OWNER/REPO/tags/?page_size=10"
# {"message":"object not found"} → repo doesn't exist
# 401/403 → private repo, need: sudo docker login
```

## Passwordless sudo (convenience)

If you need to set up NOPASSWD sudo for the current user (avoids repeated password entry):

```bash
sudo usermod -aG sudo $USER
sudo bash -c 'echo "$USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$USER && chmod 440 /etc/sudoers.d/$USER'
```

⚠️ Full NOPASSWD sudo = unrestricted root. Fine for dev machines, risky in production.

## Pitfalls

### P1: `sudo -S` + pipes break stdin
`echo 'pw' | sudo -S cmd1 | cmd2` — the second pipe consumes stdin, so sudo's password prompt may fail. **Fix:** chain commands with `&&` in a single `sudo -S sh -c '...'` or run each step as separate terminal calls.

### P2: GPG dearmor fails with "cannot open /dev/tty"
When running `gpg --dearmor` non-interactively (via pipe/script), add `--batch` flag:
```bash
sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg
```
Without `--batch`, gpg tries to open `/dev/tty` which doesn't exist in non-interactive contexts.

### P3: GPG "dearmoring failed: File exists"
If `/etc/apt/keyrings/docker.gpg` already exists, gpg won't overwrite. Add `--yes` flag or `sudo rm` the file first.

### P4: `tee` consumes stdin with sudo pipe
`echo 'pw' | sudo -S tee /path/to/file` works for the tee itself, but if you pipe content INTO it (`echo content | echo pw | sudo -S tee file`), the content pipe gets broken. **Fix:** write to `/tmp/file` first, then `sudo cp`.

### P5: Docker repo not appearing in apt update
If `apt-get update` doesn't show Docker repo, check `/etc/apt/sources.list.d/docker.list` exists and has correct content. The `tee` stdin issue (P4) often causes an empty file.

### P6: Docker Hub unreachable (IPv6 timeout or China network)
If `docker pull` fails with timeout on IPv6 address (`[2a03:2880:...]`), or China mirrors return 429/timeout:
- **Best option:** Configure proxy via systemd (see Proxy Configuration section above)
- Use mixed proxy port (e.g. 7890), not HTTP-only port — HTTP-only may fail while mixed works
- China mirrors (`docker.1ms.run`, `mirror.ccs.tencentyun.com`, etc.) are frequently rate-limited
- Verify proxy works: `curl -x http://127.0.0.1:7890 -s https://registry-1.docker.io/v2/`

### P7: Kernel upgrade pending
Docker may warn about kernel version mismatch. Not blocking but recommend reboot when convenient.

### P8: "permission denied" on docker.sock despite user in docker group
After `usermod -aG docker $USER`, the group change doesn't take effect until a new login session. But even after re-login, the socket file permissions may still block `docker compose`:
```
unable to get image '...': permission denied while trying to connect to the docker API at unix:///var/run/docker.sock
```

**Quick fix:**
```bash
sudo chmod 666 /var/run/docker.sock
```

**Better fix** (persists across reboots): ensure the user is actually in the docker group and log out/in:
```bash
groups $USER   # verify 'docker' appears
# If it does but still fails, the socket permissions may have been changed by another process
sudo chown root:docker /var/run/docker.sock
sudo chmod 660 /var/run/docker.sock
newgrp docker   # activate group without logout
```

The `chmod 666` approach is fine for dev machines but opens the socket to all users. Prefer `chown root:docker` + `chmod 660` on shared systems.

## Proxy Configuration (recommended in China)

China Docker mirrors are unreliable (rate-limited, frequently down). Using a local proxy (mihomo/Clash) is more reliable.

### Step 1: Find proxy port

```bash
ps aux | grep -i clash | grep -v grep    # find process
ss -tlnp | grep -E '7890|7891|7892'      # find listening ports
```

Common mihomo/Clash ports:
- `7890` — mixed (HTTP + SOCKS5) ← **use this one**
- `7891` — HTTP only
- `7892` — SOCKS5 only
- `9090` — API/dashboard

### Step 2: Verify proxy works for Docker Hub

```bash
curl -x http://127.0.0.1:7890 -s --connect-timeout 10 https://registry-1.docker.io/v2/
# Should return: {"errors":[{"code":"UNAUTHORIZED",...}]}  ← means it works
```

If HTTP-only port fails but mixed port works, always use mixed port (7890).

### Step 3: Configure Docker systemd proxy

```bash
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/proxy.conf <<'EOF'
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7890"
Environment="HTTPS_PROXY=http://127.0.0.1:7890"
Environment="NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
```

> **Note:** Do NOT put proxy in `/etc/docker/daemon.json` — that file is for registry mirrors and other daemon config. Systemd override is the correct place for proxy env vars.

### Step 4: Clear any mirror config if present

If `daemon.json` has `registry-mirrors` from previous attempts, clear them to avoid conflicts:
```bash
sudo tee /etc/docker/daemon.json <<'EOF'
{}
EOF
sudo systemctl restart docker
```

### China Mirror Fallback (if no proxy available)

China mirrors are hit-or-miss. If you must try:
```json
// /etc/docker/daemon.json
{
  "registry-mirrors": [
    "https://mirror.ccs.tencentyun.com",
    "https://registry.docker-cn.com",
    "https://docker.1ms.run"
  ]
}
```
Expect rate limiting (429) on many mirrors.
