Ubuntu 26.04 Tutorial / Run a Container App with Persistent Data and Restricted Ports
Container images provide the runtime environment, while persistent data and published ports need separate decisions. This exercise installs Docker Engine and Compose on a dedicated Ubuntu 26.04 VM and keeps a host-only Nginx app’s data across container recreation.
Choose the installation source and privilege model
On a server already using Docker, containerd, or Kubernetes, inventory containers, volumes, repositories, and packages before planning changes. Do not blindly remove packages or mix installation methods. The procedure below targets a fresh Ubuntu VM; stop if docker.sources or docker.asc already exists at the indicated paths.
Docker group membership and socket access grant powerful host privileges. This lesson uses explicit sudo rather than making the socket world-writable or adding every user to the docker group. Treat image execution as a privileged trust decision.
Install from the official repository
Save the complete script as ~/install-docker-lab.sh and run bash ~/install-docker-lab.sh. It stops before writing if the release or existing-file checks fail. Signed-By scopes the repository key to this source.
set -eu
. /etc/os-release
if [ "$ID" != ubuntu ] || [ "$VERSION_ID" != 26.04 ]; then
printf "This lab requires Ubuntu 26.04.\n" >&2
exit 1
fi
if [ -e /etc/apt/sources.list.d/docker.sources ] || [ -e /etc/apt/keyrings/docker.asc ]; then
printf "Inspect the existing Docker repository first.\n" >&2
exit 1
fi
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: ${UBUNTU_CODENAME:-$VERSION_CODENAME}
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo docker version
sudo docker compose version
sudo docker run --rm hello-world
Verify both client/server versions and the hello-world success message. Resolve package, signature, or network failures before continuing. Record actual installed versions; removing a repository does not uninstall the engine or delete its data.
Keep the Nginx site-configuration lab on 8080 and use host port 8081 for this container, retaining its internal port 80. If 8081 is occupied, inspect its owner with ss/compose ps and consistently substitute another verified host port in Compose and curl.
Define the Compose app and volume
Create a new ~/compose-lab directory and save the following as compose.yaml inside it. First use ss -lnt to check that 127.0.0.1:8081 is free. Omitting the host address may publish to other interfaces, so this lab explicitly binds loopback.
services:
web:
image: nginx:stable-alpine
ports:
- "127.0.0.1:8081:80"
volumes:
- lab-html:/usr/share/nginx/html
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
lab-html:
lab-html is a named volume separate from the container’s writable layer. The exercise starts with initial content and then writes its own index.html. The stable-alpine tag can change; reproducible deployments need a tested digest and update process. Adjust the example log limits to the workload.
Run, recreate, and verify persistent data
(
set -eu
cd ~/compose-lab
sudo docker compose config
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail 30 web
)
Start the app after configuration validation and image download succeed. Check container state and logs, then write the lab content below. Do not overwrite files in an existing project or volume containing real data.
(
set -eu
cd "$HOME/compose-lab"
sudo docker compose exec web sh -c 'printf "volume-lab\n" > /usr/share/nginx/html/index.html'
curl --fail http://127.0.0.1:8081/
sudo docker compose up -d --force-recreate
curl --fail http://127.0.0.1:8081/
sudo docker image inspect nginx:stable-alpine --format '{{json .RepoDigests}}'
)
Each start, recreation, and shutdown block changes into ~/compose-lab before running. A directory change inside parentheses does not persist after the block ends. Every Compose command must use the same compose.yaml to target the same project and volume. The volume-lab response before and after recreation shows that the app reads the same volume file. This does not prove a successful backup or complete application health. Another machine should not reach this loopback-published port; inspect actual bindings with ss and docker compose ps.
Distinguish stopping from deleting data
(
set -eu
cd "$HOME/compose-lab"
sudo docker compose down
sudo docker volume ls
)
down removes this project’s containers and networks while retaining named volumes by default. Start again with up -d to verify the response. Do not use down -v or volume prune as routine stop commands: they can delete data. Coordinate application writes and test restoration to a separate location when backing up volumes.




