3165 views
# DevNet Lab 11 -- Build a Sample Web App in a Podman Container [toc] --- ### Scenario This lab will cover basic Bash scripting techniques, which are a prerequisite for the rest of the course. The lab progression follows these major steps, all of which you will complete in the DevNet virtual machine: 1. Create and run a Python script for a simple web application. 2. Create a Bash script to automate creating a Dockerfile, building the Podman container, and running the container. 3. Finally, use the `podman` command to explore the Podman application container instance. ![Rootless Podman container](https://md.inetdoc.net/uploads/6e5bbf8e-da84-4449-8759-5c694cb31fe6.png) Running an application or application container in the DevNet virtual machine is an intermediate step toward continuous integration. The ultimate goal is to build and run applications on separate worker nodes that can be disposed of at will. ### Objectives After completing this lab, students will be able to: - Write and run Bash scripts to automate tasks, including creating and modifying files and directories. - Develop a simple Python-based web application using the Flask framework. - Build and deploy containerized applications using Podman, including creating Dockerfiles. - Manage and interact with containers, including starting, stopping, and accessing their environments. ## Part 1: Create a Simple Bash Script Bash knowledge is crucial for working with continuous integration, continuous deployment, containers, and with your development environment. Bash scripts help programmers automate a variety of tasks in one script file. In this part, you will briefly review how to create a Bash script. Later in the lab, you will use a Bash script to automate the creation of a web app inside of a Podman container. ### Step 1: Create an empty Bash script file 1. Change your working directory to `~/labs/lab11/sample-app` and add a new file called `user-input.sh`. ```bash mkdir -p ~/labs/lab11/sample-app && cd ~/labs/lab11 ``` 2. Open the file in a text editor. Open it in a text editor such as vim, Visual Studio Code with the remote SSH extension. 3. Add the ‘shebang’ to the top of the script. From here you can enter commands for your bash script. Add the ‘shebang’ which tells the system that this file includes commands that need to be run in the bash shell. ```bash #!/usr/bin/env bash ``` > Note: You can use a graphical text editor such as Visual Studio Code. However, you should be familiar with command-line text editors like nano and vim. 4. Add simple bash commands to the script. Enter some simple Bash commands for your script. The following commands will ask the user for a name, set the name to a variable called `user_name`, and display a string of text with the user’s name. ```bash= #!/bin/bash echo -n "Enter your name: " read -r user_name echo "Hello, ${user_name}. Be my friend!" exit 0 ``` 5. Save your script and exit your text editor. 6. Run your script from the command line. You can run it directly from the command line using the following command. ```bash bash user-input.sh ``` ```bash= Enter your name: Alice Hello, Alice. Be my friend! ``` ### Step 2: Change the mode of the script to an executable file for all users Change the mode of the script to an executable using the `chmod` command. Set the options to `a+x` to make the script executable (x) by all users (a). After using chmod, notice permissions have been modified for users, groups, and others to include the "x" (executable). ```bash ls -l user-input.sh -rw-rw-r-- 1 etu etu 108 feb. 8 14:20 user-input.sh ``` ```bash chmod a+x user-input.sh ls -l user-input.sh -rwxrwxr-x 1 etu etu 108 feb. 8 14:20 user-input.sh ``` ```bash ./user-input.sh ``` ```bash= Enter your name: Bob Hello, Bob. Be my friend! ``` ### Step 3: Investigate other bash scripts If you have little or no experience creating bash scripts, take some time to search the internet for bash tutorials, bash examples, and bash games. ## Part 2: Create a Sample Web App Before we can launch an application in a **Podman container**, we first need to have the app. In this part, you will create a very simple Python script that will display the IP address of the client when the client visits the web page. ### Step 1: Install Flask and open a port on the Virtual Machine Web application developers using Python typically use a framework. A framework is a library of code that makes it easier for developers to build reliable, scalable, and maintainable web applications. Flask is a web application framework written in Python. You will use this framework to create the sample web app. Flask receives requests and then provides a response to the user in the web app. This is useful for dynamic web applications because it allows for user interaction and dynamic content. This sample web app is dynamic because it displays the client’s IP address and port number. > Note: Understanding Flask's functions, methods, and libraries is beyond the scope of this course. It is used in this lab to show how quickly you can get a web application up and running. If you want to learn more, search the web for more information and tutorials on the Flask framework. You will use a Python virtual environment to run the Flask service. In the command list below, the working directory (VSCode workspace) is located in `$HOME/labs/lab11`. After initializing a new Git repository for this lab, start by creating the `pyproject.toml` and `.gitignore` files. The `pyproject.toml` defines `flask` as the main library of this lab Python virtual environment. ```bash cat << EOF >pyproject.toml [project] name = "Lab11" version = "0.1.0" description = "Build a Sample Web App in a Podman Container" requires-python = ">=3.13" dependencies = ["flask"] EOF ``` The `.gitignore` file defines caches and binaries that should not be included in the version control system. ```bash cat << EOF >.gitignore .venv/ __pycache__/ *.pyc .pytest_cache EOF ``` :::info For a detailed introduction to UV installation and configuration, refer to [Lab 5 – Explore Python virtual environments with UV](https://md.inetdoc.net/s/f4P_Oy4yo). ::: Install the dependencies and set up the virtual environment. ```bash uv lock --upgrade ``` ```bash= Using CPython 3.13.12 Resolved 9 packages in 300ms ``` ```bash uv sync ``` ```bash= Using CPython 3.13.12 Creating virtual environment at: .venv Resolved 9 packages in 1ms Prepared 1 package in 365ms Installed 7 packages in 30ms + blinker==1.9.0 + click==8.4.1 + flask==3.1.3 + itsdangerous==2.2.0 + jinja2==3.1.6 + markupsafe==3.0.3 + werkzeug==3.1.8 ``` If your IDE does not activate the new virtual environment automatically, you can do so manually. ```bash source .venv/bin/activate ``` Open a new terminal and verify the `python` command belongs to the newly created virtual environment. ```bash command -v python ``` ```bash= /home/etu/labs/lab11/.venv/bin/python ``` You are now ready to start coding. ### Step 2: Open the `sample-app.py` file 1. Open a new `sample-app.py` file located in the `sample-app/` directory. 2. Add the commands to import the methods from Flask. Add the following commands to import the required methods from the flask library. ```python= from flask import Flask, request ``` 3. Create an instance of the Flask class. Create an instance of the Flask class and name it `sample`. Be sure to use two underscores before and after the name (`__name__`). ```python= sample = Flask(__name__) ``` 4. Define a method to get the client's IP address. Next, configure Flask to display a message with the client's IP address when a user visits the default page (root directory). ```python= @sample.route("/") def main(): return f"You are calling me from {request.remote_addr}\n" ``` > Note the @sample.route("/") Flask statement. Frameworks like Flask use a routing technique (.route) to point to an application URL (this is not to be confused with network routing). Here the "/" (root directory) is bound to the main() function. So, when the user goes to the `http://localhost:8081/` (root directory) URL, the output of the return statement will be displayed in the browser. 5. Configure the app to run locally. Finally, configure Flask to run the application locally at `http://0.0.0.0:8081`, which is also `http://localhost:8081`. Make sure you use two underscores before and after `name`, and before and after `main`. ```python= if __name__ == "__main__": sample.run(host="::", port=8081) ``` ### Step 3: Save and run your sample web app Save your script and run it from the command line. You should see the following output, indicating that your “sample-app” server is running. If you do not see the following output, or if you get an error message, check your `sample-app.py` script carefully. ```bash python sample-app/sample-app.py ``` ```bash= * Serving Flask app 'sample-app' * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on all addresses (::) * Running on http://[::1]:8081 * Running on http://[2001:678:3fc:34:baad:caff:fefe:0]:8081 Press CTRL+C to quit ```` ### Step 4: Verify the server is running You can verify the server is running in one of two ways. * Open a web browser and enter `localhost:8081` in the URL field. You should get the following output: ```bash= You are calling me from ::1 ``` If you receive an "HTTP 400 Bad Request" response, check your `sample-app.py` script carefully. * Open another terminal window and use the command-line URL tool (cURL) to verify the server’s response. ```bash curl http://localhost:8081 ``` ```bash= You are calling me from ::1 ``` You can also list the open sockets listening on port 8081. ```bash lsof -i tcp:8081 ``` ```bash= COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME python3 6475 etu 3u IPv6 23039 0t0 TCP *:tproxy (LISTEN) ``` ```bash ss -tanp sport = :8081 ``` ```bash= State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 128 *:8081 *:* users:(("python",pid=21358,fd=3)) ``` ### Step 5: Stop the server Return to the terminal window where the server is running and press CTRL+C to stop the server. ## Part 3: Configure the Web App to use Website Files In this part, build out the sample web app to include an `index.html` page and `style.css` specification. The `index.html` is typically the first page loaded in a client’s web browser when visiting your website. The `style.css` is a style sheet used to customize the look of the web page. ### Step 1: Explore the directories that will be used by the web app Create the Web app folders starting from the Git repository directory, for example `$HOME/labs/lab11/`. ```bash mkdir -p sample-app/templates sample-app/static ``` Create the Website's main index page content. ```bash= cat << 'EOF' >sample-app/templates/index.html <!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Sample app Flask</title> <link rel="stylesheet" href="/static/style.css" /> </head> <body> <main class="card"> <h1>Network and Transport Layer Information</h1> <div class="grid"> <article class="panel"> <h2>Source</h2> <p> <span>IP Address</span> <strong>{{ netinfo.source_ip }}</strong> </p> <p><span>Port</span> <strong>{{ netinfo.source_port }}</strong></p> </article> <article class="panel"> <h2>Destination</h2> <p> <span>IP Address</span> <strong>{{ netinfo.destination_ip }}</strong> </p> <p> <span>Port</span> <strong>{{ netinfo.destination_port }}</strong> </p> </article> </div> </main> </body> </html> EOF ``` Note that the final empty line is necessary for the web page to be downloaded as complete HTML. Create the CSS stylesheet. ```bash= cat << EOF >sample-app/static/style.css :root { --bg-color: #efe2cf; --surface: #fffdf9; --ink: #15323f; --lagoon: #0f7a8a; } body { margin: 0; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; color: var(--ink); background-color: var(--bg-color); display: flex; align-items: center; justify-content: center; } .card { background: var(--surface); border-radius: 12px; padding: 2.5rem; box-shadow: 0 10px 30px rgba(21, 50, 63, 0.15); width: min(1200px, 95vw); } h1 { font-size: 1.4rem; color: var(--lagoon); margin-top: 0; border-bottom: 2px solid #eee; padding-bottom: 1rem; } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; margin-top: 1.5rem; } .panel h2 { font-size: 1.1rem; margin-bottom: 1rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.8; } .panel p { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 1rem; margin: 0.5rem 0; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; } .panel span { opacity: 0.8; white-space: nowrap; } .panel strong { font-family: monospace; font-size: 1.1rem; color: var(--lagoon); word-break: break-all; max-width: 100%; } EOF ``` ### Step 2: Update the Python code for the sample web app Now that you have explored the basic website files, you will need to update the `sample-app.py` file to render the `index.html` file instead of just returning data. Generating HTML content from Python code can be tedious, especially when using conditional statements or repeating structures. The HTML file can be automatically rendered in Flask using the `render_template` function. This requires importing the `render_template` method from the Flask library and editing it to include the return function. Create the `sample-app/sample-app.py` script file with the following content: ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- from flask import Flask, render_template, request sample = Flask(__name__) @sample.route("/") def main(): # Get the real source IP (useful when traffic goes through a proxy) client_ip = ( request.headers.get("X-Forwarded-For", request.remote_addr or "") .split(",")[0] .strip() ) netinfo = { "source_ip": client_ip or "N/A", "source_port": request.environ.get("REMOTE_PORT", "N/A"), "destination_ip": request.environ.get("SERVER_ADDR") or request.environ.get("SERVER_NAME", "N/A"), "destination_port": request.environ.get("SERVER_PORT", "N/A"), } return render_template("index.html", netinfo=netinfo) if __name__ == "__main__": sample.run(host="::", port=8081) ``` ### Step 3: Save and run your script Save and run your `sample-app.py` script. You should get output like the following: ```bash python sample-app/sample-app.py ``` ```bash= * Serving Flask app 'sample-app' * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on all addresses (::) * Running on http://[::1]:8081 * Running on http://[2001:678:3fc:VVVV:baad:caff:fefe:XXXX]:8081 Press CTRL+C to quit ``` > Note: If you got traceback output and an error message with something like `OSError: [Errno 98] Address already in use`, then you have not shut down your previous server. Return to the terminal window where this server is running and press CTRL+C to kill the server process. Run your script again. ### Step 4: Verify your program is running Again, there are two ways to verify that your program is running. - Open a web browser and type `localhost:8081` in the URL field. You should see the same output as before. ![Sample app in browser](https://md.inetdoc.net/uploads/25929ad8-dce7-4730-934b-741584337b83.png) - Open another terminal window and use the `curl` command to check the server's response. Here you will see the result of the HTML code automatically rendered by the `render_template` function. ```bash curl http://localhost:8081 ``` ### Step 5: Stop the server Return to the terminal window where the server is running and press CTRL+C to stop the server. ## Part 4: Create a Bash Script to Build and Run a Podman Container An application can be deployed on a bare-metal server (physical server dedicated to a single-tenant environment) or in a virtual machine, as you just did in the previous part. It can also be deployed in a containerized solution such as **Podman**. In this part, you will create a bash script and add commands to it to perform the following tasks to create and run a Podman container: * Create temporary directories to store the website files * Copy the website directories and sample-app.py into the temporary directory * Create a Dockerfile * Build the Podman container * Run the container and verify that it works ### Step 1: Install Podman and enable lingering for user account Start by installing the necessary packages and enabling lingering for your regular user account. This allows you to manage containers even when no user session is open. ```bash sudo apt -y install podman podman-docker ``` ```bash sudo loginctl enable-linger 1000 sudo reboot ``` Rebooting is recommended because installing the container management tools may trigger an initramfs rebuild, and all user sessions must be closed after activating lingering. :::info The `loginctl enable-linger 1000` command enables lingering for user 1000. This ensures that their systemd user services start at boot time and continue running even after all their login sessions end. This is important for maintaining long-running user processes, such as background daemons or timers. ::: ### Step 2: Create temporary directories to store the website files Open the `sample-app.sh` Bash script file in the `$HOME/labs/lab11/sample-app` directory. Add the "shebang" and the commands to create a directory structure with a temporary directory. ```bash= #!/usr/bin/env bash set -euo pipefail if ! command -v podman >/dev/null 2>&1; then echo "Error: podman is required but not installed." >&2 exit 1 fi tempdir=$(mktemp -d) trap 'rm -rf "${tempdir}"' EXIT ``` Copy the website directories and `sample-app.py` into the temporary directory. In the same `sample-app.sh` file, add the following commands to copy the website directories and script into `${tempdir}/`. ```bash= # Copy application files cp sample-app.py "${tempdir}/" cp -r templates static "${tempdir}/" ``` ### Step 3: Create a Dockerfile In this step, you will add the necessary commands in the `sample-app.sh` file to create a **Dockerfile** in the `tempdir`. This Dockerfile will be used to build the container. ```bash= # Generate Dockerfile cat <<'EOF' >"${tempdir}/Dockerfile" FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim WORKDIR /app COPY . /app/ # Install dependencies directly in the system environment via uv RUN uv pip install --system flask EXPOSE 8081 CMD ["python3", "sample-app.py"] EOF ``` This heredoc writes a complete Dockerfile into `${tempdir}` with the following characteristics: * Uses the `uv` Python base image `ghcr.io/astral-sh/uv:python3.13-trixie-slim`. * Copies all application files, including `sample-app.py`, `templates`, and `static` into `/app`. * Installs the **Flask** dependency directly in the container’s system Python environment via `uv`. * Exposes TCP port 8081, which is used by the Flask application. * Configures the container to start the app with `python3 sample-app.py`. Using a heredoc keeps the Dockerfile definition close to the script logic and avoids managing a separate Dockerfile on disk under version control for this lab. ### Step 4: Build and start the Podman container Add the following commands to `sample-app.sh` to switch to `${tempdir}/` and build the Podman container image. ```bash # Build image podman build -f "${tempdir}/Dockerfile" -t sampleapp "${tempdir}" # Cleanup and run if podman ps -a --format '{{.Names}}' | grep -qx 'samplerunning'; then podman rm -f samplerunning fi podman run -td -p 8081:8081 --name samplerunning sampleapp ``` Build the image and start the `samplerunning` container using the `sample-app.sh` script. ```bash cd sample-app/ ``` ```bash= /home/etu/labs/lab11/sample-app ``` ```bash bash sample-app.sh ``` ```bash= STEP 1/6: FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim Trying to pull ghcr.io/astral-sh/uv:python3.13-trixie-slim... Getting image source signatures Copying blob fa28f97bd6f6 done | Copying blob 5b4d6ff92fc4 done | Copying blob db744c23eac5 done | Copying blob f3b83f2d9173 done | Copying blob e4f4b0eb5b0d done | Copying config 952fe157e8 done | Writing manifest to image destination STEP 2/6: WORKDIR /app --> 1495c5f639a6 STEP 3/6: COPY . /app/ --> 34acdc9a3ce9 STEP 4/6: RUN uv pip install --system flask Using Python 3.13.13 environment at: /usr/local Resolved 7 packages in 631ms Prepared 7 packages in 190ms Installed 7 packages in 14ms + blinker==1.9.0 + click==8.4.1 + flask==3.1.3 + itsdangerous==2.2.0 + jinja2==3.1.6 + markupsafe==3.0.3 + werkzeug==3.1.8 --> c0ac658b08a6 STEP 5/6: EXPOSE 8081 --> 5200689dbe62 STEP 6/6: CMD ["python3", "sample-app.py"] COMMIT sampleapp --> 1d646b1cf97f Successfully tagged localhost/sampleapp:latest 1d646b1cf97f53fc32c032a24811738e026cbfba997ae0c55a5630c73563130f 9d330f553fea0ab9f1cfd5e1fa2a13fb215901232f0172e62e180acb61a6eb6d ```` Verify that the container is running. ```bash podman ps -a ``` ```bash= CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 9d330f553fea localhost/sampleapp:latest python3 sample-ap... About a minute ago Up About a minute 0.0.0.0:8081->8081/tcp samplerunning ``` Verify you have access to the web app on port 8081. Using a shell `curl` command: ```bash curl -sS -w '\n%{http_code}' http://localhost:8081 |\ { out=$(cat); echo "$(tail -n1 <<< "$out") \ $(grep -m1 -oP '(?<=<title>).*?(?=</title>)' <<< "$out")"; } ``` ```bash= 200 Sample app Flask ``` Using your browser: ![Sample app container access](https://md.inetdoc.net/uploads/1ecf364f-bc73-4aa1-bc1d-1e9996da8e2c.png) ## Part 5: Investigate the running Podman container and the web app By default, Podman runs containers as user processes within a **systemd** user *slice*, where you can see the `python3 sample-app.py` process. ```bash systemctl --user status ``` ```bash= ● devnet26 State: running Units: 124 loaded (incl. loaded aliases) Jobs: 0 queued Failed: 0 units Since: Wed 2026-06-03 09:54:31 CEST; 6 days ago systemd: 260.1-1 Tainted: unmerged-bin CGroup: /user.slice/user-1000.slice/user@1000.service ├─init.scope │ ├─703 /usr/lib/systemd/systemd --user --deserialize=10 │ └─717 "(sd-pam)" ├─session.slice │ └─dbus.service │ └─161546 /usr/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only └─user.slice ├─libpod-9d330f553fea0ab9f1cfd5e1fa2a13fb215901232f0172e62e180acb61a6eb6d.scope │ └─container │ └─427991 python3 sample-app.py ├─libpod-conmon-9d330f553fea0ab9f1cfd5e1fa2a13fb215901232f0172e62e180acb61a6eb6d.scope │ └─427989 /usr/bin/conmon --api-version 1 -c 9d330f553fea0ab9f1cfd5e1fa2a13fb215901232f0172e62e180acb61a6eb6d -u 9d330f553fea0ab9f1cfd5e1fa2a13fb215901232f0172e62e180acb61a6eb6d -r /usr/bin/crun -b /home/etu/.local/share/containers/storage/overlay-containers/9d330f553fea0ab9f1cfd5e1fa2a13fb215901232f0172e62e180acb61a6eb6d/userdata -p /run/user/1000/containers/overlay-containers/9d330f553fea0ab9f1cfd5e1fa2a13fb215901232f0172e62e180acb61a6eb6d/userdata/pidfile -n samplerunning --exit-dir /run/user/1000/libpod/tmp/exits --persist-dir /run/user/1000/libpod/tmp/persist/9d330f553fea0ab9f1cfd5e1fa2a13fb215901232f0172e62e180acb61a6eb6d --full-attach -s -l journald --log-level warning --syslog --runtime-arg --log-format=json --runtime-arg --log --runtime-arg=/run/user/1000/containers/overlay-containers/9d330f553fea0ab9f1cfd5e1fa2a13fb215901232f0172e62e180acb61a6eb6d/userdata/oci-log -t --conmon-pidfile /run/user/1000/containers/overlay-containers/9d330f553fea0ab9f1cfd5e1fa2a13fb215901232f0172e62e180acb61a6eb6d/userdata/conmon.pid --exit-command /usr/bin/podman --exit-command-arg --root --exit-command-arg /home/etu/.local/share/containers/storage --exit-command-arg --runroot --exit-command-arg /run/user/1000/containers --exit-command-arg --log-level --exit-command-arg warning --exit-command-arg --cgroup-manager --exit-command-arg systemd --exit-command-arg --tmpdir --exit-command-arg /run/user/1000/libpod/tmp --exit-command-arg --network-config-dir --exit-command-arg "" --exit-command-arg --network-backend --exit-command-arg netavark --exit-command-arg --volumepath --exit-command-arg /home/etu/.local/share/containers/storage/volumes --exit-command-arg --db-backend --exit-command-arg sqlite --exit-command-arg --transient-store=false --exit-command-arg --hooks-dir --exit-command-arg /usr/share/containers/oci/hooks.d --exit-command-arg --runtime --exit-command-arg crun --exit-command-arg --storage-driver --exit-command-arg overlay --exit-command-arg --events-backend --exit-command-arg journald --exit-command-arg container --exit-command-arg cleanup --exit-command-arg --stopped-only --exit-command-arg 9d330f553fea0ab9f1cfd5e1fa2a13fb215901232f0172e62e180acb61a6eb6d ├─podman-427962.scope │ └─427986 /usr/bin/pasta --config-net -t 8081-8081:8081-8081 --dns-forward 169.254.1.1 -u none -T none -U none --no-map-gw --quiet --netns /run/user/1000/netns/netns-24d36185-37ab-9fd3-596a-b157ee389a0d --map-guest-addr 169.254.1.2 └─podman-pause-51881ab5.scope └─363189 catatonit -P ``` * Line 16: New user slice. * Line 19: Python script that runs the dynamic web page. ### Step 1: Access and explore the running container Remember that a Podman container encapsulates everything needed to run your application, allowing for easy deployment in various environments beyond just your virtual machine. To access a running container, use the `podman exec -it` command and specify the container's name (samplerunning) and the desired shell (/bin/bash). The `-i` option indicates interactive mode, and the `-t` option indicates terminal access. The prompt will change to `root@containerID`. Your container ID will differ from the one shown below. Note that the container ID matches the ID shown in the podman ps -a output. ```bash podman exec -it samplerunning /bin/bash ``` ```bash= root@9d330f553fea:/app# ``` You now have root access to the `samplerunning` Podman container. From here, you can use Linux commands to explore it. For example, type `ls` to view the directory structure at the app level. ```bash ls -1 ``` ```bash= Dockerfile sample-app.py static templates ``` Remember that you added commands to the Dockerfile in your Bash script to copy your application's directories and files to the `app/` directory. Exit the Podman container to return to the DevNet virtual machine shell. ```bash exit ``` ### Step 2: Stop and remove the Podman container To stop a Podman container, enter the `podman stop` command and specify the name of the running container. The container will then take a few seconds to clean up the cache. To verify that the container is still running, enter the `podman ps -a` command. However, when you refresh the web page at `http://localhost:8081`, you will see that the web app is no longer running. ```bash podman stop samplerunning ``` ```bash= WARN[0010] StopSignal SIGTERM failed to stop container samplerunning in 10 seconds, resorting to SIGKILL ``` This warning is harmless in the context of this lab. Print the status of your `samplerunning` container. ```bash podman ps -a --format '{{.Status}}' ``` ```bash= Exited (137) 3 minutes ago ``` The above output shows the container process has exited. You can restart a stopped container with the `podman start` command. The container will immediately spin up. ```bash podman start samplerunning ``` ```bash= samplerunning ``` To permanently remove the container, first stop it, then use the `podman rm` command to remove it. You can rebuild it by running the `sample-app.sh` script again. Use the `podman ps -a` command to verify that the container has been removed. ```bash podman stop samplerunning ``` ```bash= WARN[0010] StopSignal SIGTERM failed to stop container samplerunning in 10 seconds, resorting to SIGKILL samplerunning ``` ```bash podman rm samplerunning ``` ```bash= samplerunning ``` ```bash podman ps -a ``` ```bash= CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ``` To manage container images, you can use the `podman image` command: ```bash podman images ``` ```bash= REPOSITORY TAG IMAGE ID CREATED SIZE localhost/sampleapp latest 1d646b1cf97f 29 minutes ago 184 MB ghcr.io/astral-sh/uv python3.13-trixie-slim 952fe157e84d 5 days ago 180 MB ``` ```bash podman image rm localhost/sampleapp:latest ``` ```bash= Untagged: localhost/sampleapp:latest Deleted: 1d646b1cf97f53fc32c032a24811738e026cbfba997ae0c55a5630c73563130f Deleted: 5200689dbe62083cb972a55e4182ada05de84ae0e829b35373f19e291ea23982 Deleted: c0ac658b08a635d5a6cd1420dd646aeec992f95d9b983c8d26c51ff8b102f13a Deleted: 34acdc9a3ce908174c1383ead22e9135a3b744d22798b60531a14a48a7022c1c Deleted: 1495c5f639a6e351457999f9e1901630ce20d926a0c3675147d85c5b5e098a0a ``` ## Conclusion In this lab, students gained hands-on experience with scripting, web application development, and Podman containerization. They learned to automate workflows with Bash scripts, develop dynamic web applications in Python, and deploy those applications in containers. These skills are fundamental to modern software development and DevOps practices. To scale their applications further, students are encouraged to explore advanced container orchestration tools, such as Kubernetes.