Install Docker Engine and Compose with safer defaults
Install from Docker's signed repository, limit daemon exposure, bound logs, and document privilege boundaries.
Provide a maintainable runtime without silently exposing a privileged daemon or unbounded logs.
- Ubuntu Server 24.04 LTS
- Docker Engine 29.x
- Compose plugin 2.x
- Supported host Confirm Ubuntu release and architecture.
- Package audit Identify conflicting unofficial packages and active containers.
- Privilege decision docker group membership is root-equivalent; prefer sudo or rootless mode.
OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.
Full guide
What you will build
- A Docker Engine 29.x installation on Ubuntu 24.04 from Docker's signed APT repository, including the CLI, containerd, Buildx and Compose v2 plugin.
- A validated daemon policy with bounded local logs, live restore and no-new-privileges as the default for newly created containers.
- A least-privilege Compose baseline and an explicit decision that Docker socket access remains restricted to trusted administrators.
- docker version and docker compose version report a working client/server pair from the intended repository.
- The daemon is active, no unauthenticated TCP API listens on 2375 or 2376, and AppArmor plus the built-in seccomp profile appear in security options.
- A disposable hello-world image runs successfully and is removed when it exits.
- New Compose workloads inherit bounded logging and no-new-privileges while individual services drop capabilities and use read-only filesystems where supported.
Architecture
How the parts fit together
The Docker CLI and Compose plugin send privileged API requests through a root-owned Unix socket to dockerd. Dockerd coordinates containerd, OCI runtimes, network rules, image storage, volumes and container logs on the host.
- An administrator invokes docker or docker compose through sudo.
- The CLI sends a request over /var/run/docker.sock to the privileged daemon.
- dockerd pulls or builds content, asks containerd to create the container, and programs storage and networking.
- Kernel namespaces, cgroups, capabilities, AppArmor and seccomp constrain the process according to daemon and workload policy.
Assumptions
- The host is a supported 64-bit Ubuntu 24.04 system and is not an unsupported derivative.
- Conflicting docker.io, podman-docker, containerd or runc packages and any existing /var/lib/docker state have been inventoried before repository changes.
- Docker's published-port firewall behavior is understood. The current Docker documentation warns that exposed container ports can bypass UFW or firewalld policy and that Docker manages iptables-compatible rules.
- The host uses sudo for daemon control. Rootless mode is evaluated separately if unprivileged operation is a requirement.
- A maintenance window exists for the deliberate daemon restart, and every existing workload has a recovery or restart plan.
Key concepts
- Docker daemon
- The privileged service that accepts API requests and controls host resources. Access to it can be used to gain full host control.
- Image
- An immutable content-addressed filesystem and metadata template used to create containers.
- Container
- A host process with isolation and resource controls, not a virtual-machine security boundary by itself.
- Capability
- A discrete Linux privilege. Dropping all and adding only required capabilities reduces the power of a compromised process.
- Seccomp and AppArmor
- Kernel controls that restrict system calls and filesystem or process actions in addition to namespaces and capabilities.
- Live restore
- A daemon feature that can keep compatible containers running while dockerd is unavailable, but does not guarantee application continuity for every upgrade or configuration change.
Before you copy
Values used in this guide
{{ubuntuCodename}}Ubuntu release codename used in Docker's APT source.
Example: nobleSecurity and production boundaries
- Members of the docker group can mount the host root filesystem or control privileged containers. Treat the group as an administrator boundary, not a convenience permission.
- Never expose the daemon on plain TCP. Even a firewall-limited daemon endpoint can be reachable from containers and must use authenticated TLS or an SSH transport.
- A container running as root still maps to powerful host identities unless rootless or user namespace mechanisms are deliberately configured.
- Docker manipulates host packet-filtering rules for published ports. Design the host firewall and DOCKER-USER policy with Docker rather than assuming UFW alone controls exposure.
Stop before continuing if
- Stop if an existing container runtime, production workload or /var/lib/docker data set has not been inventoried and backed up.
- Stop if the Docker APT key or repository source cannot be verified or does not match the host architecture and Ubuntu codename.
- Stop if daemon.json is invalid or Docker cannot restart; restore the previous file before further changes.
- Stop if the daemon binds to TCP 2375/2376 unexpectedly or security options lose the expected AppArmor/seccomp controls.
- Stop before adding a user to the docker group unless root-equivalent access is explicitly approved.
command
Install repository prerequisites
Install CA certificates, curl, and the apt keyring directory.
Why this step matters
Install only the tools needed to verify HTTPS downloads and create a scoped APT keyring before trusting a new package source.
What to understand
ca-certificates supplies the TLS trust store and curl retrieves the official signing key. The dedicated keyring directory avoids the deprecated global apt-key trust model.
The directory is world-readable but root-writable; APT's unprivileged helpers must be able to read repository keys.
System changes
- Refreshes APT metadata, installs ca-certificates and curl, and creates /etc/apt/keyrings with mode 0755.
Syntax explained
apt install --yes- Installs named packages and accepts the package confirmation.
install -m 0755 -d- Creates the directory with explicit permissions.
sudo apt update && sudo apt install --yes ca-certificates curl && sudo install -m 0755 -d /etc/apt/keyrings/etc/apt/keyrings created
Checkpoint: Confirm prerequisite paths
curl --version | head -n 1 && stat -c '%U:%G %a %n' /etc/apt/keyringsContinue whencurl is available and the keyring directory is root-owned mode 755.
Stop whenAPT is incomplete or the directory is writable by non-root users.
Security notes
- This step changes packages but does not yet trust Docker's repository.
Alternatives
- Use an approved internal mirror that preserves signature verification.
Stop conditions
- Resolve package-manager errors before adding another source.
config
Add Docker's signing key and repository
Use Docker's official deb822 repository definition, scoped to its signing key and the verified Ubuntu codename.
Why this step matters
Scope Docker's signing key to one deb822 source so the key cannot authenticate unrelated repositories.
What to understand
curl fails on HTTP errors, follows the official redirect and writes the ASCII-armored key to the dedicated keyring.
The docker.sources file binds URI, suite, architecture and Signed-By path. Replace the codename only with the value reported by /etc/os-release.
System changes
- Creates /etc/apt/keyrings/docker.asc and /etc/apt/sources.list.d/docker.sources.
Syntax explained
curl --fail --show-error --location- Fails on HTTP errors, retains diagnostics and follows redirects.
--output- Writes the key to the exact root-controlled path.
Signed-By- Limits repository authentication to Docker's dedicated key.
Architectures- Restricts packages to the host architecture reported by dpkg.
/etc/apt/sources.list.d/docker.sourcesValues stay on this page and are never sent or saved.
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: {{ubuntuCodename}}
Components: stable
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.ascsudo curl --fail --show-error --location https://download.docker.com/linux/ubuntu/gpg --output /etc/apt/keyrings/docker.asc && sudo chmod a+r /etc/apt/keyrings/docker.asc/etc/apt/keyrings/docker.asc downloaded Docker repository definition saved for noble
Checkpoint: Inspect source and key fingerprint
gpg --show-keys --with-fingerprint /etc/apt/keyrings/docker.asc && cat /etc/apt/sources.list.d/docker.sourcesContinue whenThe official Docker key is readable and the source uses download.docker.com, the host architecture and intended Ubuntu codename.
Stop whenThe fingerprint, URI, architecture, suite or Signed-By path is unexpected.
If this step fails
APT reports NO_PUBKEY or conflicting Signed-By values.
Likely causeThe key is unreadable, the path is wrong, or an older Docker source exists with a different key configuration.
ls -l /etc/apt/keyrings/docker.ascgrep -Rns 'download.docker.com' /etc/apt/sources.list /etc/apt/sources.list.d
ResolutionRemove only the obsolete duplicate source, correct key readability and refresh APT metadata.
Security notes
- Do not disable signature verification to work around a repository error.
Alternatives
- Pin the official repository through an internal signed mirror under organizational control.
Stop conditions
- Do not install until apt-cache policy shows the expected Docker origin.
command
Install Engine and plugins
Install Engine, CLI, containerd, Buildx, and Compose.
Why this step matters
Install the daemon, matching CLI, bundled containerd and supported plugins as one coherent package set.
What to understand
docker-ce provides the daemon, docker-ce-cli the client, containerd.io the tested runtime bundle, and the two plugins provide build and Compose workflows.
For production, list available versions and pin an approved version instead of automatically taking a new major release.
System changes
- Installs and starts Docker Engine, creates the docker group and socket, and initializes runtime directories under /var/lib/docker and /var/lib/containerd.
Syntax explained
docker-ce / docker-ce-cli- Install Docker's daemon and matching command-line client.
containerd.io- Install Docker's supported containerd and runtime dependency bundle.
docker-buildx-plugin- Adds extended BuildKit-based build commands.
docker-compose-plugin- Adds Compose v2 as docker compose.
sudo apt update && sudo apt install --yes docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginSetting up docker-ce ... Setting up docker-compose-plugin ...
Checkpoint: Verify package origin and service
apt-cache policy docker-ce | sed -n '1,12p' && systemctl is-active dockerContinue whenThe candidate originates from download.docker.com and the daemon is active.
Stop whenPackages come from an unintended source or the service is failed.
If this step fails
Docker installation completes but the service is failed.
Likely causeA conflicting runtime, storage driver, firewall setup or old daemon configuration prevents startup.
systemctl status docker --no-pagersudo journalctl -u docker -n 100 --no-pagersudo dockerd --validate
ResolutionCorrect the reported conflict without deleting runtime data, then start the service and repeat package verification.
Security notes
- The privileged daemon and its socket now exist; keep access root-controlled.
Alternatives
- Install an explicitly pinned package version after testing upgrades.
Stop conditions
- Do not configure workloads while daemon health or package origin is ambiguous.
config
Set bounded logging and live restore
Prevent unbounded logs and never expose the Docker TCP API without a documented mTLS design.
Why this step matters
Host-wide defaults reduce log exhaustion and privilege gain for every new container, while live restore can reduce disruption during daemon maintenance.
What to understand
The local log driver rotates files using max-size and max-file. Existing containers retain the logging configuration they were created with.
no-new-privileges prevents processes from gaining privileges through setuid or file capabilities. Workloads that require those mechanisms must be reviewed rather than globally exempted.
System changes
- Creates /etc/docker/daemon.json and changes defaults applied after daemon validation and restart.
Syntax explained
log-driver: local- Uses Docker's optimized local log format with rotation support.
max-size: 10m- Rotates each log segment around ten megabytes.
max-file: 3- Retains at most three local rotated segments per container.
live-restore: true- Allows compatible containers to remain running while the daemon is unavailable.
no-new-privileges: true- Sets the Linux no-new-privileges bit by default for new containers.
/etc/docker/daemon.json{
"log-driver": "local",
"log-opts": { "max-size": "10m", "max-file": "3" },
"live-restore": true,
"no-new-privileges": true
}daemon.json saved as valid JSON
Checkpoint: Validate the candidate without restarting
jq empty /etc/docker/daemon.json && sudo dockerd --validate --config-file=/etc/docker/daemon.jsonContinue whenJSON parsing succeeds and dockerd reports configuration validation successful.
Stop whenJSON is invalid, an option is unknown or conflicts with a daemon flag.
Security notes
- Do not add hosts entries that publish the daemon TCP API.
Alternatives
- Override logging or privileges per service only when the exception is documented and tested.
Stop conditions
- Back up an existing daemon.json and validate the complete merged replacement before restart.
verification
Validate JSON and restart deliberately
A restart may affect workloads that cannot use live restore.
Why this step matters
A controlled restart is the point where daemon defaults take effect, so gate it on validation and immediately prove service recovery.
What to understand
jq catches JSON syntax and dockerd validation catches daemon-specific issues. The restart may disrupt unmanaged or incompatible workloads despite live restore.
Inspect existing containers before the window and confirm them afterward; active service state alone does not prove workload health.
System changes
- Restarts the privileged Docker daemon and applies daemon.json defaults for subsequently created containers.
Syntax explained
jq empty- Parses JSON and emits nothing on success.
systemctl restart docker- Stops and starts the daemon under systemd.
systemctl is-active- Requires the resulting unit state to be active.
jq empty /etc/docker/daemon.json && sudo systemctl restart docker && systemctl is-active dockeractive
Checkpoint: Confirm daemon and existing workloads
systemctl is-active docker && sudo docker ps --format 'table {{.Names}}\t{{.Status}}'Continue whenDocker is active and every expected workload is present with a healthy status.
Stop whenThe daemon is failed or a known workload is missing or unhealthy.
If this step fails
Restart leaves docker.service failed.
Likely causedaemon.json is invalid at runtime or conflicts with systemd flags.
sudo journalctl -u docker -n 100 --no-pagersystemctl cat dockersudo dockerd --validate --config-file=/etc/docker/daemon.json
ResolutionRestore the previous daemon.json, restart Docker and verify workloads before reworking the candidate.
Security notes
- Preserve /var/lib/docker; configuration rollback must not become destructive data cleanup.
Alternatives
- Reload only options documented as reloadable, but still verify their effective behavior.
Stop conditions
- Rollback immediately if critical containers do not recover.
verification
Verify versions and security options
Record Engine, Compose, AppArmor, and seccomp state.
Why this step matters
Record the actual client, server, Compose and kernel security controls so future incidents and upgrades have a known baseline.
What to understand
docker version distinguishes client from server. Compose is a plugin with its own release. docker info exposes enabled security options.
The Go template returns JSON text suitable for unambiguous recording. Missing AppArmor can be legitimate on another distribution but is unexpected in this Ubuntu baseline.
System changes
- No persistent changes; queries daemon and plugin metadata.
Syntax explained
docker version- Shows client and daemon API versions.
docker compose version- Shows the installed Compose v2 plugin version.
docker info --format- Prints selected daemon information using a Go template.
{{json .SecurityOptions}}- Serializes the daemon security-options field as JSON.
sudo docker version && sudo docker compose version && sudo docker info --format '{{json .SecurityOptions}}'Docker Engine 28.x Docker Compose v2.x ["name=apparmor","name=seccomp,profile=builtin"]
Checkpoint: Save the security baseline
sudo docker version && sudo docker compose version && sudo docker info --format '{{json .SecurityOptions}}'Continue whenEngine 29.x, Compose v2, and expected AppArmor plus builtin seccomp options are visible.
Stop whenClient/server versions are incompatible or expected kernel controls are absent.
Security notes
- Version metadata is safe to record, but do not publish full docker info output without reviewing host details.
Alternatives
- Export the selected fields into inventory management.
Stop conditions
- Investigate missing seccomp/AppArmor before production workloads.
command
Run a disposable smoke test
Run and remove the official hello-world container.
Why this step matters
A disposable official sample proves registry pull, image extraction, runtime creation, network egress and log delivery end to end.
What to understand
Docker pulls hello-world only if absent, creates a short-lived container and prints a confirmation. --rm removes the stopped container, not the image.
This is a platform smoke test, not a security assessment of arbitrary images. Production images need provenance and vulnerability controls.
System changes
- May download the hello-world image and creates then removes one disposable container.
Syntax explained
docker run- Creates and starts a container from an image.
--rm- Removes the container automatically after it exits.
hello-world- Selects Docker's small installation-test image.
sudo docker run --rm hello-worldHello from Docker!
Checkpoint: Require the expected greeting
sudo docker run --rm hello-worldContinue whenHello from Docker! and exit code 0.
Stop whenPull, DNS, storage, runtime or container execution fails.
Security notes
- Do not generalize successful execution of one image into trust of all registry content.
Alternatives
- Use an internally mirrored, signed smoke image in restricted environments.
Stop conditions
- Resolve platform errors before deploying Compose workloads.
config
Create a least-privilege Compose baseline
Pin images, drop capabilities, use read-only filesystems, and keep secrets outside the file.
Why this step matters
A compact baseline demonstrates the controls each application team should start from rather than relying only on daemon defaults.
What to understand
read_only protects the image filesystem, tmpfs supplies writable temporary space, and cap_drop removes kernel capabilities. Applications may need explicit writable volumes or a narrowly added capability.
Pin production images by digest after testing. A semantic tag alone can move to different content.
System changes
- Creates compose.yaml for an example service; no container is started by this step.
Syntax explained
image- Selects the tested image reference; add an immutable digest in production.
read_only- Mounts the container root filesystem read-only.
cap_drop: [ALL]- Removes all Linux capabilities before any explicit additions.
no-new-privileges:true- Prevents process privilege gains inside the container.
tmpfs: [/tmp]- Provides ephemeral writable memory-backed storage for temporary files.
restart: unless-stopped- Restarts after failures or reboot unless an operator intentionally stopped it.
compose.yamlservices:
app:
image: example/app:1.2.3
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
tmpfs: [/tmp]
restart: unless-stoppeddocker compose config renders one hardened service
Checkpoint: Render the effective Compose model
docker compose config --quiet && docker compose configContinue whenValidation succeeds and the rendered service contains the reviewed restrictions without secret values.
Stop whenThe service needs privileged mode, broad host mounts or unresolved values.
Security notes
- A read-only filesystem and dropped capabilities are defense in depth, not proof that an image is trustworthy.
Alternatives
- Document the minimum capability or writable mount when an application cannot run under this baseline.
Stop conditions
- Do not weaken all controls merely to make an unknown image start.
warning
Treat Docker access as root access
Do not add general users to the docker group as a convenience.
Why this step matters
Make the privilege boundary explicit because adding a user to the docker group silently grants practical control over the host.
What to understand
A Docker API user can bind-mount /, access devices or start privileged containers. Socket permissions therefore protect a root-equivalent interface.
Use named administrators with sudo audit trails or rootless mode; never chmod the socket to make a permission error disappear.
System changes
- No automatic change; records the access decision and prevents an unsafe convenience action.
Privilege decision recorded
Checkpoint: Audit socket access
getent group docker && stat -c '%U:%G %a %n' /var/run/docker.sockContinue whenOnly approved administrators are members and the socket is root:docker mode 660 or stricter.
Stop whenA general user, service account or web process has daemon access without root-equivalent approval.
Security notes
- Treat any exposed docker.sock mount as a critical host-control path.
Alternatives
- Use sudo with command logging or deploy documented rootless mode.
Stop conditions
- Remove unauthorized access and review daemon audit evidence before continuing.
Finish line
Verification checklist
systemctl is-active dockerReturns active.docker compose versionReports Compose v2.ss -lntp | grep -E ':2375|:2376' || trueNo unexpected Docker TCP listener.Recovery guidance
Common problems and safe checks
APT reports conflicting packages or cannot install containerd.io.
Likely causeDistribution containerd, runc, docker.io, podman-docker or an old Compose package remains installed.
dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runcapt-cache policy docker-ce containerd.io
ResolutionPlan removal of conflicting packages and preserve existing runtime data; do not purge /var/lib/docker or /var/lib/containerd.
Docker fails to start after daemon.json is installed.
Likely causeThe JSON is malformed, an option conflicts with a systemd command-line flag, or a configured feature is unsupported.
jq empty /etc/docker/daemon.jsonsudo dockerd --validate --config-file=/etc/docker/daemon.jsonsudo journalctl -u docker -n 100 --no-pager
ResolutionRestore the backed-up file, start Docker, then correct and validate the candidate before another controlled restart.
docker commands return permission denied on /var/run/docker.sock.
Likely causeThe operator is not root and is intentionally not a member of the root-equivalent docker group.
stat -c '%U:%G %a %n' /var/run/docker.sockid
ResolutionUse sudo for the administrative command or deploy rootless mode; do not broaden socket permissions.
A container port is reachable despite an expected UFW deny.
Likely causeDocker's published-port rules bypass the UFW path, as documented for the supported installation.
docker ps --format '{{.Names}} {{.Ports}}'sudo iptables -S DOCKER-USERsudo ss -lntup
ResolutionRemove unintended published ports and implement reviewed filtering in the Docker-compatible path, including DOCKER-USER where appropriate.
Reference
Frequently asked questions
Is membership in the docker group safer than sudo?
No. Docker control can mount host filesystems and start highly privileged containers, so group membership is effectively root-equivalent.
Does live-restore mean restarting Docker is risk-free?
No. It can keep compatible containers running during daemon unavailability, but networking, plugins, upgrades and application behavior still require a maintenance plan.
Why use the local log driver?
It provides built-in rotation and a compact format for local operation. Centralized logging still requires an explicit collection and retention design.
Recovery
Rollback
Restore the previous daemon configuration or package set.
- Stop Compose applications cleanly.
- Restore and validate /etc/docker/daemon.json.
- Restart Docker and verify known workloads.
- Preserve /var/lib/docker until data retention is decided.
Evidence