Lesson 07 VM Deploy with docker-compose
Theme:

Homework — VM Deploy with docker-compose

Lesson 07 · Cloud Infrastructure & DevOps · 3 tasks

Task 1 — Pin a specific image tag and roll back

Switch the API service in docker-compose.prod.yml from the hard-coded :latest tag to a parameterised tag driven by an environment variable:

services:
  api:
    image: ghcr.io/${GHCR_OWNER}/url-shortener-api:${API_TAG:-latest}
  worker:
    image: ghcr.io/${GHCR_OWNER}/url-shortener-worker:${WORKER_TAG:-latest}

Add API_TAG=v0.1.0 and WORKER_TAG=v0.1.0 to your .env file on the VM. Then:

  1. Push a small code change to main in your fork and wait for CI to publish a new tag (v0.1.1).
  2. Deploy forward: update API_TAG=v0.1.1 in .env, run docker compose pull && up -d, verify with curl /healthz.
  3. Roll back: set API_TAG=v0.1.0, run docker compose pull && up -d, verify again.

Self-check: docker compose -f docker-compose.prod.yml images shows the API row with the tag matching whatever API_TAG you set last. Rolling back works exactly the same as rolling forward — same three commands, different variable value.

Task 2 — Add a fail2ban-style SSH lockout

A VM with SSH exposed on port 22 will receive automated brute-force attempts within minutes of creation. fail2ban monitors authentication log files and temporarily bans IPs that exceed a configurable failure threshold.

Install and enable fail2ban on the VM:

sudo apt-get install -y fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

Confirm the default sshd jail is enabled (the output should show Currently failed and Total failed counters). Then deliberately fail SSH authentication five times from a second terminal (use a wrong password or key), and confirm your IP appears in the banned list:

sudo fail2ban-client status sshd   # should show your IP in 'Banned IP list'

Unban yourself:

sudo fail2ban-client set sshd unbanip <your-ip>

Self-check: sudo fail2ban-client status sshd shows your test IP in “Banned IP list” after the failed attempts and gone after the unban command. Document the steps, the jail configuration, and the unban command in a /srv/url-shortener/OPS.md file committed to your fork.

Task 3 — Externalise Postgres data to a dedicated bind mount

Today, Postgres data lives in a Docker named volume pg_data on the VM’s root disk. Named volumes are convenient but hard to snapshot independently. Replace the named volume with a bind mount to a fixed host path:

# docker-compose.prod.yml — postgres service (after):
services:
  postgres:
    volumes:
      - /var/lib/url-shortener/postgres:/var/lib/postgresql/data

Before bringing the stack up with the new config, create the host directory with the correct ownership (the Postgres container runs as uid 999):

sudo mkdir -p /var/lib/url-shortener/postgres
sudo chown 999:999 /var/lib/url-shortener/postgres

Migrate the existing data from the named volume to the bind mount, then take a manual snapshot of the underlying disk via your cloud provider’s console. Verify the data survives by attaching the snapshot to a new VM and running the API against it.

Self-check: after the swap, docker compose -f docker-compose.prod.yml exec postgres ls /var/lib/postgresql/data shows the same DB files; a psql round-trip confirms the data is present; the snapshot exists in your cloud console. Bonus: write a one-line cron entry to snapshot daily.

View solutions →