Cloud AI Campus
  • Career paths
  • Learning paths
  • Hands-on Labs
Log in Sign up

๐Ÿงช Hands-on lab · 45 min

Docker โ€” Real-World Applications

  1. 1. PHP + Apache
  2. 2. Node.js with environment variables
  3. 3. A multi-port service
  4. 4. Reading logs + restart policies

PHP + Apache

php:<version>-apache is one of the most-used images on Docker Hub. Apache is pre-installed and configured to serve /var/www/html/, with mod_php enabled for .php files.

Build a tiny PHP app:

mkdir -p ~/php-app/src && cd ~/php-app
cat > src/index.php <<'PHP'
<?php
header('Content-Type: text/plain');
echo "PHP " . PHP_VERSION . " running in a container\n";
echo "Hostname: " . gethostname() . "\n";
echo "Loaded extensions: " . count(get_loaded_extensions()) . "\n";
PHP

cat > Dockerfile <<'EOF'
FROM php:8-apache
COPY src/ /var/www/html/
EXPOSE 80
EOF

Build + run:

docker build -t cac-php-app .
docker run -d --name php-app -p 8090:80 cac-php-app
curl -s http://localhost:8090/

You should see the PHP version, the container's hostname, and a loaded-extensions count. That's a full PHP request lifecycle running on a containerized stack you built in seconds.

Click Verify step.

Hint

FROM php:8-apache; COPY your src/ into /var/www/html/.

Node.js with environment variables

Configuration belongs in environment variables, not in the image or in source. Docker's -e KEY=VALUE at runtime injects them.

Build a minimal Node service:

mkdir -p ~/node-app && cd ~/node-app
cat > package.json <<'JSON'
{
  "name": "cac-node-app",
  "version": "1.0.0",
  "main": "server.js"
}
JSON

cat > server.js <<'JS'
const http = require('http');
const env = process.env.NODE_ENV || 'development';
const port = Number(process.env.PORT || 3000);
http.createServer((_, res) => {
  res.setHeader('Content-Type', 'text/plain');
  res.end(`Node ${process.version}\nNODE_ENV=${env}\nPORT=${port}\n`);
}).listen(port, () => console.log(`listening on ${port} as ${env}`));
JS

cat > Dockerfile <<'EOF'
FROM node:lts-alpine
WORKDIR /src/app
COPY package.json ./
RUN npm install --omit=dev --no-audit --no-fund
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]
EOF

Build + run with NODE_ENV=production:

docker build -t cac-node-app .
docker run -d --name node-app \
    -e NODE_ENV=production \
    -p 3000:3000 \
    cac-node-app

curl -s http://localhost:3000/

You should see NODE_ENV=production in the response. To run the same image as a dev instance, just pass a different -e:

docker run --rm -e NODE_ENV=development -p 3001:3000 cac-node-app &
sleep 1 && curl -s http://localhost:3001/

Same image, different behaviour โ€” that's the dependency-injection pattern Docker is built around.

Click Verify step once :3000 reports NODE_ENV=production.

Hint

FROM node:lts-alpine; install deps in the image; pass `-e NODE_ENV=production` at runtime.

A multi-port service

Some images expose multiple ports โ€” Jenkins listens on 8080 for HTTP and 50000 for its agent protocol; databases often have admin + client ports. Each -p publishes one host:container port pair.

For this lab the publishable target is httpd (lightweight, two listeners on demand). Jenkins LTS is the real-world reference but pulls ~600MB; we'll stand up a smaller proxy that demonstrates the multi-port pattern.

docker run -d --name multi-svc \
    -p 8100:80 \
    -p 8101:80 \
    httpd:2.4

Both :8100 and :8101 reach the same container's port 80:

curl -sI http://localhost:8100/ | head -1
curl -sI http://localhost:8101/ | head -1

In a real Jenkins setup the same pattern publishes two different listeners:

# (Reference โ€” don't run for this lab, ~600MB pull)
# docker run -d --name jenkins \
#     -p 8080:8080 \
#     -p 50000:50000 \
#     jenkins/jenkins:lts

Inspect the published ports:

docker port multi-svc
docker inspect --format '{{.NetworkSettings.Ports}}' multi-svc

Click Verify step when both :8100 and :8101 answer.

Hint

Repeat `-p` to publish multiple ports (e.g. -p 8080:8080 -p 5000:5000).

Reading logs + restart policies

Two operational primitives you'll touch every day in production.

Logs. Anything an app writes to stdout/stderr is captured by the container runtime. View it with docker logs:

docker logs php-app
docker logs --tail 20 php-app           # last 20 lines
docker logs -f php-app &                # follow (Ctrl-C to stop)
curl -s http://localhost:8090/ >/dev/null   # generate a request
curl -s http://localhost:8090/ >/dev/null
sleep 1; kill %1 2>/dev/null

Apache writes a line per request. Same idea applies to nginx, Node, Python โ€” anything that logs to stdout is captured.

Restart policies. Containers don't restart by default. Crash = gone. Four policies, set with --restart:

| Policy | When does it restart? | |--------------------|------------------------| | no (default) | never | | on-failure | only on non-zero exit | | unless-stopped | always, unless you docker stop it explicitly | | always | always โ€” even if explicitly stopped (resumes after daemon restart) |

Start a deliberately-crashing container with unless-stopped and watch it come back:

docker run -d --name flaky --restart=unless-stopped alpine sh -c "sleep 3; exit 1"
sleep 4
docker inspect -f '{{.RestartCount}}' flaky
sleep 4
docker inspect -f '{{.RestartCount}}' flaky

RestartCount keeps climbing โ€” Docker keeps reviving it. To stop the loop:

docker stop flaky
docker rm flaky

Click Verify step when the php-app logs show at least one curl request from this scenario.

Hint

`docker logs <name>`; pass `--restart unless-stopped` to keep a service alive.

© 2026 Cloud AI Campus