Introduction
Containers package and run applications, but packaging alone does not choose a safe process identity. This prepared DeskLite worker generates a small ticket report and currently runs as root.
You will learn the minimum Docker workflow, then rebuild and run the worker with a non-root identity and only the file access its task needs. Docker and a fixed base image are prepared; no registry account or prior Docker experience is required.
Meet the Prepared Container Project
In this step, you will identify the files that make up the reporting task.
An image is a packaged filesystem and startup configuration. A container is a running instance of an image, with its own processes and writable filesystem layer. A Dockerfile describes how to build an image. A tag, such as ticket-report:practice, is a convenient name for an image; it can be moved to a new image after a rebuild.
cd /home/labex/project/ticket-worker
ls
Expect Dockerfile, worker.sh, input, and output. The prepared worker reads CSV rows, writes their count to a report, then stays running so we can inspect it.
cat input/tickets.csv
There are three ticket rows after the header. The expected report is tickets=3.
cat Dockerfile
FROM selects the cached base image, WORKDIR selects the image's working directory, and COPY adds the worker. USER root chooses the process identity. CMD is the default startup command, represented as a list. You will change only USER.
The base tag desklite-base:1.37.0 is already cached from a fixed BusyBox image. All subsequent builds and runs use local files. Docker commands use sudo to access this lab's daemon. That host-level access is powerful; it is separate from the application identity we are reducing inside the container.
Build and Run the Existing Worker
In this step, you will establish the working but over-privileged baseline.
docker build reads the Dockerfile and a build context, the directory of files available to COPY. Here . means the current directory. -t assigns the local image tag:
sudo docker build -t ticket-report:practice .
Expect a successful build. The image exists, but no worker is running yet.
docker run creates a container. -d runs it in the background, and --name gives it a reusable name. --network none disables networking because this report needs only files.
A bind mount exposes a host path at a container path. Each --mount describes its type, source (src), and destination (dst). This baseline exposes both directories as writable because no readonly option is present. We will restrict the input later.
sudo docker run -d --name ticket-worker --network none --mount type=bind,src=/home/labex/project/ticket-worker/input,dst=/data/input --mount type=bind,src=/home/labex/project/ticket-worker/output,dst=/data/output ticket-report:practice
Docker prints a container ID. It is not the worker's user ID. The original worker should generate its report, which we will inspect next.
Inspect the Actual Runtime Identity
In this step, you will distinguish a successful report from a least-privilege runtime.
docker ps lists running containers:
sudo docker ps
Find ticket-worker. docker logs reads the application's standard output:
sudo docker logs ticket-worker
Expect worker uid=0 and tickets=3. UID 0 is root. The business task works, but it uses more privilege than needed.
docker exec runs an inspection command in this running container. id -u prints its effective numeric user ID:
sudo docker exec ticket-worker id -u
Expect 0 again. docker inspect reads configuration. --format selects one field; copy the quoted template exactly. .Config.User is the configured user:
sudo docker inspect --format '{{.Config.User}}' ticket-worker
Expect root. The image setting and the real process inspection agree. A file saying "non-root" would not provide this evidence.
Rebuild with the Prepared Non-Root User
In this step, you will set the image's default application identity.
The cached base already includes nobody, UID 65534. No user creation or password is needed. Open the Dockerfile:
nano Dockerfile
Change only the USER instruction. The full file should be:
FROM desklite-base:1.37.0
WORKDIR /app
COPY worker.sh /app/worker.sh
USER nobody
CMD ["sh", "/app/worker.sh"]
Save with Ctrl+O, Enter, and exit with Ctrl+X.
sudo docker build -t ticket-report:practice .
The tag now identifies the rebuilt image. Existing containers do not automatically switch to a rebuilt image; the old root container is still the old instance. In the next step you will replace it and give the new identity the required output access.
Replace the Container with Restricted Mounts
In this step, you will combine the non-root identity with working file permissions.
docker rm removes a container. -f first stops it if running. Remove only this exercise's named container:
sudo docker rm -f ticket-worker
The report directory was initially root-owned. As in the file-permissions lab, a non-root process needs ownership and permission to write its output. chown -R changes the directory and its existing report recursively to UID and GID 65534. Mode 750 gives the owner full access without public write access:
sudo chown -R 65534:65534 output
sudo chmod 750 output
Now create the replacement. The input mount adds readonly; the output mount remains writable. This prevents input modification while allowing the report to be generated:
sudo docker run -d --name ticket-worker --network none --mount type=bind,src=/home/labex/project/ticket-worker/input,dst=/data/input,readonly --mount type=bind,src=/home/labex/project/ticket-worker/output,dst=/data/output ticket-report:practice
Expect a new container ID. The image now supplies the non-root user; no runtime root override is needed.
Verify Identity, Access, and Business Output
In this step, you will check all three parts of the result on the replacement container.
sudo docker exec ticket-worker id -u
Expect 65534. Check the worker's actual output:
sudo docker logs ticket-worker
Expect worker uid=65534 and tickets=3. Read the real output file through the container:
sudo docker exec ticket-worker cat /data/output/report.txt
Expect tickets=3. This proves that reducing privilege did not break the task.
Inspect the mounts. The quoted template selects .Mounts and formats it as JSON:
sudo docker inspect --format '{{json .Mounts}}' ticket-worker
Find /data/input with "RW":false and /data/output with "RW":true. RW means read/write. The same input bytes are available for reading, but the mount denies writing.
Try a harmless file creation to test that restriction. touch would create an empty file if permitted:
sudo docker exec ticket-worker touch /data/input/probe.txt
Expect a Read-only file system error and a nonzero exit. This one denial is intentional. Do not make the input writable to remove it; the report already succeeded using read-only input.
Confirm the Final Worker Is Still Running
In this step, you will leave the intended worker ready for inspection.
sudo docker ps
The named container ticket-worker should be running. Confirm the configured user and report once more:
sudo docker inspect --format '{{.Config.User}}' ticket-worker
sudo docker exec ticket-worker cat /data/output/report.txt
Expect nobody and tickets=3. The course check also compares the running image with your delivery tag, checks the actual worker PID, and tests the original input and output. Rebuilding a Dockerfile without replacing the container is insufficient.
This exercise reduces application identity and file access. It does not make Docker daemon access unprivileged or establish a complete production container-hardening policy.
Summary
You built a real image with a non-root application identity, ran that image with restricted input and working output access, and verified the actual process and report. Image configuration, runtime identity, mount modes, and functional behavior all contribute to the final state.



