guides/docker
Infrastructure · Containers · Self-hosting

Docker

Container runtime and the foundation under virtually every self-hosted service. Package an application with all its dependencies into an isolated, reproducible image and run it identically on any Linux host.

Engine: Open Source Linux Docker Inc.
Imageimmutable template
Containerrunning instance
Composemulti-container stacks

Concepts

Three core terms you meet constantly: an image is an immutable template (code + dependencies), a container is a running instance of an image, and Compose describes one or more containers declaratively in a YAML file so the whole stack starts with a single command.

For your stack: Both n8n and Miniflux are easiest to deploy as Docker Compose stacks — this guide is the foundation for both.

Installation

Ubuntu / Debian

Use Docker's official apt repo rather than the distro package — it's newer and includes Compose v2 as a plugin.

1 · Remove old packages & add prerequisites
sudo apt remove docker docker-engine docker.io containerd runc # if present
sudo apt update
sudo apt install ca-certificates curl gnupg -y
2 · Add Docker's GPG key & repo
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) \
  signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
On Zorin/Debian derivatives: if $VERSION_CODENAME isn't recognized by the repo, set it manually to the Ubuntu/Debian base your version is built on (e.g. noble or bookworm).
3 · Install Engine + Compose plugin
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin -y
4 · Verify
sudo docker run --rm hello-world
docker compose version

Post-install

Run Docker without sudo and let the daemon start at boot.

Add your user to the docker group
sudo usermod -aG docker $USER
# log out/in or:
newgrp docker
Enable at boot
sudo systemctl enable --now docker
Security note: Membership of the docker group is effectively equivalent to root access (you can mount the host file system into a container). Grant access only to trusted users, and consider rootless mode on multi-user systems.

Docker Compose

deklarativt

Describe your stack in compose.yaml and control it with docker compose. Example skeleton:

compose.yaml (minimal)
services:
  app:
    image: nginx:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./data:/usr/share/nginx/html:ro
    networks: [ web ]

networks:
  web:
Bind to 127.0.0.1 so the port is local only and exposed solely via your Nginx reverse proxy — not directly to the internet.
KommandoFunktion
docker compose up -dStart the stack in the background (detached)
docker compose downStop and remove containers + networks
docker compose pullPull the latest images
docker compose logs -fFollow logs live
docker compose psStatus of the stack's services
docker compose restartRestart services
docker compose exec app shOpen a shell in a running container
Update routine: docker compose pull && docker compose up -d pulls new images and recreates only the containers that changed.

CLI reference

KommandoFunktion
docker ps / docker ps -aRunning / all containers
docker imagesList local images
docker logs -f NAMEFollow a container's logs
docker exec -it NAME shInteractive shell in a container
docker stop / start / restart NAMELifecycle management
docker rm NAME / docker rmi IMAGERemove container / image
docker inspect NAMEFull JSON metadata (network, mounts, config)
docker statsLive CPU/RAM/IO per container
docker system dfDisk usage broken down by images/containers/volumes
docker system prune -aClear unused images/containers/networks (frees space)
Careful with prune -a: It removes all images not in use by a container — including ones you want to keep. Add --volumes only if you deliberately want to delete unmounted volumes (can mean data loss).

Networking

Containers on the same user-defined network can reach each other via service name as hostname — that's how n8n finds its database.

TypeUse
bridge (default)Default isolated network per Compose stack; service name = DNS
hostShares the host network stack directly (no isolation — avoid if possible)
noneNo networking — full isolation
internalBridge without outbound internet — good for databases
Pattern: Put the database on an internal network so it can't reach the internet, and the application on both the internal and a proxy network. Never expose the database port on the host.

Volumes & persistence

Containers are ephemeral — data that must survive a recreation has to go in a volume or bind mount.

Named volume (Docker-managed)
docker volume create n8n_data
docker volume ls
docker volume inspect n8n_data
Bind mount (host path)
# in compose: ./data:/app/data
# gives you direct access to the files on the host
Backup: Named volumes live under /var/lib/docker/volumes/. For your restic/pCloud stack you can either back up that path, or better: docker compose exec db pg_dump … for consistent database dumps rather than copying raw files while the database is running.

Hardening

ControlRecommendation
Port-bindingBind to 127.0.0.1, expose only via a reverse proxy
restart-policyunless-stopped so services come back up after reboot
Image-tagsPin to specific versions, not blindly latest, in production
read-onlyread_only: true + tmpfs where possible
capabilitiescap_drop: [ALL] and add back only what is needed
no-new-privilegessecurity_opt: [no-new-privileges:true]
UFWNote: Docker manipulates iptables and can bypass UFW rules — bind to localhost rather than relying on UFW alone
OpdateringRegular pull + recreate; monitor base image CVEs
The Docker + UFW trap: Docker writes its own iptables rules and can publish ports past your UFW rules. If you bind a port to 0.0.0.0, it's often open to the internet regardless of UFW. The fix: always bind to 127.0.0.1:PORT for internal services, or use the ufw-docker project to correct the rule ordering.