Skip to content

Filipanderssondev/Container_Stack_Deployment_With_Ansible

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

135 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Container Stack Deployment With Ansible

Abstract

This project demonstrates how to deploy a container-based fullstack web application within a virtual environment using Ansible and Podman. We'll make an application stack including NGINX, PostgreSQL, and a monitoring solution with Prometheus and Grafana. All components are automated through reusable Ansible roles.



Container Stack Deployment With Ansible
Authors: Filip Andersson and Jonatan Högild
24-02-2026

Table of Contents

  1. Introduction
  2. Goals and Objectives
  3. Method
  4. Target Audience
  5. Document Status
  6. Disclaimer
  7. Scope and Limitations
  8. Environment
  9. Acknowledgments
  10. Implementation
    10.1 Configuring reusable roles
    10.2 The stack
    10.3 Backend and DB
    10.4 Deployment
    10.5 Https/Tls Configuration
  11. Conclusion
  12. References
    12.1 Other projects in our virtual IT-enviroment

Introduction

Welcome friend!
In this project we are going to deploy a container-based interactive web application using infrastructure-as-code (IaC), Ansible. We will deploy the fullstack containers on our application VM that will serve as our runtime enviroment and the monitoring containers on our metrics VM serving as our metrics collector. Everything will be managed from our management VM running Ansible. This will be done by configuring Ansible roles, and reusing those roles in playbooks. This is our fourth project in a series of projects with the end goal of setting up a complete virtualized, automated, and monitored IT-Enviroment as a part of our internship on The Swedish Meteorological and Hydrological Institute (SMHI) IT-department at the headquarters in Norrköping. The second goal of these projects are also supposed to serve as a set-up guide here on Github for anyone and everyone that wants to replicate what we have done. we will link every project to each other aswell.

Other projects in our virtual IT-enviroment

Goals and Objectives

The goals and objectives of this project is:

  • To run a container-based web application / full stack, with log in, displaying multiple interactive pages.
  • Collect metrics from that app to the metrics VM, displaying it in Grafana.
  • Doing it all through Ansible on the management VM

This is part of a larger ongoing IT-infrastructure project that will use Proxmox as a base, with Rocky Linux as the OS running on each virtual machine. The goal of this project is to build a complete IT-environment and gain a deeper understanding of the underlying components and their part in a larger production chain.

Method

The solution was implemented using Ansible on a management virtual machine to automate the deployment of a container-based web application and monitoring on virtual on two machines running Podman. The container stack consisted of NGINX (frontend) that served visual presentation, Postgres (Database) for storing users and logging in, and Rocky linux based python api (Backend) as our backend api handling http requests and responses. To be able to serve multiple pages in the same window, a custom Cross-Origin-Resource-Sharing (CORS) function was created inside the backend api, since the enviroment is designed like most modern enterprises with restrictive access to the internet the containers couldn't be reliant on external python libraries and packages websites, instead a custom Rocky Linux based image was constructed using dnf as package handler, with access to internal package repositorys. The stack also consist of monitoring using container based Prometheus node exporters on each vm for exporting metrics configuring prometheus to collect metrics from the node exporters and prometheus as a data source for Grafana to visualize the result. Reusable Ansible roles and playbooks were used to install dependencies, pull images from a private image reg start containers with defined ports and volumes. To collect the container images from the private image registry, an ansible login role was composed and implemented with the mechanics of fetching confidential login credentials defined in the encrypted vault file in our ansible structure.

Target Audience

  • This repo is for anyone who wants a step-by-step guide on how to deploy a modern container stack based application and monitoring stack with Ansible and Podman. This repo is also part of a larger project aimed at people interested in learning about IT infrastructure, and building such an environment from scratch.

Document Status

This repository is considered complete and officially published.
Future improvements, refinements, or corrections may be introduced through controlled updates. Any changes will be versioned and documented in the commit history.

Disclaimer

Caution

This is intended for learning, testing, and experimentation. The emphasis is not on security or creating an operational environment suitable for production.

Scope and Limitations

  • The scope is inteded to serve as an internship project and learning opportunity when it comes to working with containers, and to run applications on our virtual machines with redundance.
  • This guide is not intended for production-grade, multi-node clusters or advanced HA setups.
  • Hardware compatibility varies; If unsure, check hardware requirements before proceeding.
  • Instructions may become outdated as software updates; always verify with the official documentation.
  • Sensitive information will be withheld. This will not hinder participation in the guide.

Environment

  • Asus PN64 ax210NGW
    • Intel® Core™ i7-12700H
    • 1TB disk
    • 64 GB memory
  • FreeIPA (4.12.2)
  • Proxmox VE (9.1.1)
  • Rocky Linux (10.1)
  • Ansible (core 2.16.14)
  • Podman (5.6.0)

Packages

  • python3
  • python3-flask
  • python3-psycopg2
  • python3-dotenv
  • python3-itsdangerous

Images

creator(if)/image-name:verison_tag

  • custom built image (rocky_flask_backend:latest)
  • rocky linux (rockylinux:10-ubi-init)
  • postgres (postgres:latest)
  • custom prometheus podman exporter (prometheus-podman-exporter:latest)
  • node-exporter (prom/node-exporter:latest)
  • Prometheus (prom/prometheus:main)
  • grafana (grafana:alpine-3.22.2)
  • nginx (nginx:1.29.4)

Binaries

  • mkcert-v1.4.4-linux-amd64 (link)

Acknowledgments

Great thanks once again to our mentor Rafael for ongoing support and guidance. And thanks to Victor for insight and guidance.

Implementation

  • Note, that some details cant be disclosed due to company policy and that i will speak in general terms. For example the registry i will pull images from i will call "private-registry.com/repository"

Every config file is available under the code directory

Configuring reusable roles

What we want is resuable roles for repetetive tasks. We want a role for checking/installing latest versions of depencencies like Podman, we want a role that logs in to our private registry fetching our credentials from our encrypted vault file, We want a role for pulling images, and we want a role for running images acting as the whole container logic, our "engine" if you will. Each role will have a defaults folder with a main.yaml file and a tasks folder with a main.yaml file. All variables will be pre defined in our defaults/main as a standard fallback as is the best practice to never hardcode anything. Each tasks/main will contain the modules and define the logic for the tasks. Then, we want a role that shows running containers and a role for killing and removing containers, because it is quite a process to do manually for each container when we want to test our containers and such.

The dependencies installation role

Very straight forward basic. The key to every task in ansible playbooks or roles is the use of ansible modules, like the ansible.builtin.dnf which allows the use of dnf package manager.

---
- name: Installation of tools
  ansible.builtin.dnf:
    name:
      - podman
    state: present
  become: true

- name: Verify podman version
  ansible.builtin.command: podman --version

Login role

defaults/main.yaml

As the registry URL isnt as sensitive information as our
user credentials, its generally considered best practice to define it in our defaults and not our encrypted vault file

Out of necesity we to have the tls verify set to false, we ran into a lot of complications with certificate authentication on the vms so it is easier to do it like this so it dosent fail.

---
#Default image registry
registry_url: "www.private-container-registry.com/myrepository"  # Dummy value for demostration purpose
tlsverify: tls-verify=false
tasks/main.yaml

This is the logic for logging us in, using the module containers.podman.podman_login. Here im referring to the variables defined in our encrypted vault file.

- name: Login to private-registry
  containers.podman.podman_login:
    registry: "{{ registry_url }}"
    username: "{{ registry_username }}"
    password: "{{ registry_password }}"
    tlsverify: false

image pull role

  • Since some image paths varies, for example:
    private-registry.com/myrepository/image:tag
    private-registry.com/myrepository/manufacturer/image:tag
    Its safer to create a mechanics for building the complete image name. Defining our default values in defaults and then overriding in pur playbooks, never hardcoding anything.
defaults/main.yaml
---
default_registry: "private-registry.com/repository"

#image basics:
default_image: "myimage"
default_tag: "latest"
tlsverify: false
tasks/main.yaml

This is the mechanics for the image name builder, since we often want to pull multiple images we want to treat each image as an item in a list we overrode in our define in our playbook. This task runs once per item, tach image is treated as an item in a list, it builds the full image name (registry, optional manufacturer, image name, and version tag).

---
- name: Pull container images
  containers.podman.podman_image:
    name: >-
      {{
        default_registry
        ~ '/'
        ~ (item.get('manufacturer', '') ~ '/' if item.get('manufacturer', '') != '' else '')
        ~ item.image_name
        ~ ':'
        ~ (item.get('tag', default_tag))
      }}
    state: present

Container run role

defaults/main.yaml
---
#Default value for variables, whats the image to run + name of new container

# Default container registry
default_registry: "private-registry.com"
default_project: "project"
manufacturer: " "
image_name: my-image
tag: "latest"
container_name: my-container

#Desired container state / absent
container_state: started

#Additional settings for later
container_ports: []
container_env_vars: {}
container_volumes: []
container_restart_policy: always
container_cmd: []
container_network: " "
tls_verification: false
tasks/main.yaml
---
- name: Run container
  containers.podman.podman_container:
    name: "{{ container_name }}"
    image: "{{ default_registry }}/{{ default_project }}{% if manufacturer is defined and manufacturer | trim != '' %}/{{ manufacturer }}{% endif %}/{{ image_name }}:{{ tag }}"
    state: "{{ container_state  }}"
    ports: "{{ container_ports | default(omit)  }}"
    env: "{{ container_env_vars  }}"
    volumes: "{{ container_volumes | default(omit) }}"
    restart_policy: "{{ container_restart_policy | default('always') }}"
    user: 0
    command: "{{ container_cmd | default([]) }}"
    tls_verify: "{{ tls_verification | default }}"
    network: "{{ container_network | default(omit) }}"

Show containers

This role is very simple so we dont need default values per say so we only go with tasks for this one.

tasks/main.yaml
---
- name: Get all container names
  command: sudo podman ps -a --format "{{'{{'}}.Names{{'}}'}}"
  register: container_names
  changed_when: false

- name: Show container names
  debug:
    msg: "Container found: {{ item }}"
  loop: "{{ container_names.stdout_lines }}"
  when: container_names.stdout != ""

Container kill role

Since the kill role is partially off the show role but with some tweaks its redundant to use defaults here aswell.

tasks/main.yaml
---
- name: Get all container names
  command: podman ps -a --format "{{'{{'}}.Names{{'}}'}}"
  register: container_names
  changed_when: false

- name: Show container names
  debug:
    msg: "Container found: {{ item }}"
  loop: "{{ container_names.stdout_lines }}"
  when: container_names.stdout != ""

- name: Stop all containers
  command: podman stop {{ item }}
  loop: "{{ container_names.stdout_lines }}"
  ignore_errors: true
  when: container_names.stdout != ""

- name: Remove all containers
  command: podman rm -f {{ item }}
  loop: "{{ container_names.stdout_lines }}"
  ignore_errors: true
  when: container_names.stdout != ""

The stack

What we want is to be able to log into our container based website, and we want our frontend to display something that speaks for what this is about with multiple pages, in this case we want it to speak about that this is our intern project at SMHI and a some information on our enviroment with diagrams.

Structure

On the application vm

└── app-praktik-projekt
    ├── backend
    │   ├── app.py
    │   ├── dnf-repos
    │   │   └── yum.repos.d
    │   │       ├── epel.repo
    │   │       ├── rocky-extras.repo
    │   │       └── rocky.repo
    │   └── Dockerfile
    ├── db
    │   └── init.sql
    └── frontend
        ├── about.html
        ├── diagram.html
        ├── diagram.png
        ├── index.html
        ├── script.js
        └── style.css
index.HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>SMHI Praktikprojekt</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="card">

    <h1>
      <span class="bold-part">SMHI</span>
      Praktikprojekt 
    </h1>

    <p class="subtitle">ENTER</p>

    <form id="loginForm">
      <input id="username" placeholder="Username" required>
      <input id="password" type="password" placeholder="Password" required>
      <button type="submit">Login</button>
    </form>

  </div>

  <script src="script.js"></script>
</body>
</html>
about.html
<!DOCTYPE html>
<html>
<head>
  <title>About</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <script>
    const user = localStorage.getItem("user");
    if (!user) window.location.href = "/";
  </script>
  <div class="card">
    <h1 id="welcome"></h1>
    <p>This system is deployed using containers, Ansible and Podman.</p>
    <button onclick="goDiagram()">VIEW ARCHITECTURE</button>
  </div>

  <script src="script.js"></script>
</body>
</html>
diagram.html

This page will serve our flowchart diagram.

<!DOCTYPE html>
<html>
<head>
  <title>Architecture</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <script>
    const user = localStorage.getItem("user");
    if (!user) window.location.href = "/";
  </script>
  <div class="card">
    <h1>ARCHITECTURE</h1>
    <img src="diagram.png" class="diagram">
    <button onclick="goBack()">BACK</button>
  </div>

  <script src="script.js"></script>
</body>
</html>

style.CSS

body {
  margin: 0;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;

  font-family: 'Segoe UI', sans-serif;
  color: white;

  background: radial-gradient(circle at top left, #1f1f1f, #000000);
  animation: fadeIn 1.2s ease-in;
}
body {
  margin: 0;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;

  font-family: 'Segoe UI', sans-serif;
  color: white;

  background: radial-gradient(circle at top left, #1f1f1f, #000000);
  animation: fadeIn 1.2s ease-in;
}

.card {
  background: rgba(255, 255, 255, 0.05);
  backdrop-filter: blur(10px);
  padding: 50px;
  border-radius: 15px;
  text-align: center;
  box-shadow: 0 0 40px rgba(255,255,255,0.1);
  width: 400px;
}

h1 {
  font-size: 3rem;
  font-weight: 900;
  letter-spacing: 2px;
  margin-bottom: 10px;
}

.subtitle {
  opacity: 0.6;
  margin-bottom: 30px;
}

input {
  width: 100%;
  padding: 14px;
  margin-bottom: 20px;
  border: none;
  border-radius: 8px;
  background: #111;
  color: white;
  font-size: 1rem;
}

button {
  width: 100%;
  padding: 14px;
  border: none;
  border-radius: 8px;
  font-weight: bold;
  font-size: 1rem;
  background: white;
  color: black;
  cursor: pointer;
  transition: 0.3s;
}

button:hover {
  background: #00ffcc;
  box-shadow: 0 0 20px #00ffcc;
  transform: translateY(-2px);
}

.diagram {
  max-width: 100%;
  margin: 20px 0;
  border-radius: 10px;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

script.js

This is the javascript that makes our web application interactive and will call our backend api

const api = "http://" + window.location.hostname + ":5000";

function login() {
  const username = document.getElementById("username").value;
  const password = document.getElementById("password").value;

  if (!username || !password) {
    alert("Enter username and password");
    return;
  }

  fetch(api + "/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ username, password })
  })
  .then(response => {
    if (!response.ok) {
      return response.json().then(data => { throw new Error(data.message); });
    }
    return response.json();
  })
  .then(data => {
    localStorage.setItem("user", username);
    window.location.href = "about.html";
  })
  .catch(error => {
    alert(error.message);
    console.error("Login error:", error);
  });
}

// Handle Enter key / form submit
document.getElementById("loginForm").addEventListener("submit", function(e) {
  e.preventDefault();
  login();
});

// Display welcome message
window.onload = function () {
  const welcome = document.getElementById("welcome");
  if (welcome) {
    const user = localStorage.getItem("user");
    welcome.textContent = "WELCOME, " + user.toUpperCase();
  }
};

// Navigation
function goDiagram() { window.location.href = "diagram.html"; }
function goBack() { window.location.href = "about.html"; }

Backend and DB

Backend

Since we designed our system like an enterprise enviroment with limited access towards the internet, we could not be depended on external python libraries for our images to work. This is why we based our python backend container off of Rocky Linux because we already have access to internal package repositorys using dnf. We had access to python3-flask but not the python3-cors dependency, making the display of multiple web pages in the same window possible so we had to construct our own Cross-Origin-Resource_Sharing (CORS) in app.py under def login()

app.py
from flask import Flask, request, jsonify, make_response
import psycopg2
import os

app = Flask(__name__)

# Database connection
def get_connection():
    return psycopg2.connect(
        host=os.getenv("DB_HOST"),
        database=os.getenv("DB_NAME"),
        user=os.getenv("DB_USER"),
        password=os.getenv("DB_PASSWORD")
    )

# Global CORS handler
@app.after_request
def add_cors_headers(response):
    response.headers["Access-Control-Allow-Origin"] = "*"
    response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
    response.headers["Access-Control-Allow-Headers"] = "Content-Type"
    return response

#Login route
@app.route("/login", methods=["POST", "OPTIONS"])
def login():

    # Handle browser preflight request
    if request.method == "OPTIONS":
        return make_response("", 200)

    data = request.get_json()

    if not data:
        return jsonify({"message": "Invalid request"}), 400

    username = data.get("username")
    password = data.get("password")

    if not username or not password:
        return jsonify({"message": "Missing credentials"}), 400

    try:
        conn = get_connection()
        cur = conn.cursor()

        cur.execute(
            "SELECT 1 FROM users WHERE username = %s AND password = %s",
            (username, password)
        )

        user = cur.fetchone()

        cur.close()
        conn.close()

        if user:
            return jsonify({"message": "Login successful"}), 200
        else:
            return jsonify({"message": "Invalid credentials"}), 401

    except Exception as e:
        print("Database error:", e)
        return jsonify({"message": "Server error"}), 500


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
Dockerfile

And our Dockerfile for building the backend image, with Rocky Linux as a base image. We used the dnf repo configs from the vm and copied it to the container.

FROM private-registry.com/repository/rockylinux:10-ubi-init

COPY dnf-repos/yum.repos.d /etc/yum.repos.d

RUN dnf install -y \
    python3 \
    python3-flask \
    python3-psycopg2 \
    python3-dotenv \
    && dnf clean all

WORKDIR /app

COPY . .

EXPOSE 5000

CMD ["python3", "app.py"]

Database

The database will be defined in our playbook however for our database we want to create a table called users, note that init.sql will only run once - initiating the table.

db/init.sql
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    username TEXT UNIQUE NOT NULL,
    password TEXT NOT NULL
);

INSERT INTO users (username, password) VALUES
('filip', 'secretPasswordExmple1'),
('jonatan', 'secretPasswordExample2')
ON CONFLICT (username) DO NOTHING;

Deployment

Application

deploy_application.yaml
---
- name: Deploy application
  hosts: application
  become: true
  roles:
    - role: containers/install
    - role: containers/login/filip

  tasks:
    - name: Run postgres
      include_role:
        name: containers/run
      vars:
        container_network: app_network
        container_name: postgres_database
        image_name: postgres
        tag: latest
        container_ports:
          - "5433:5432"
        container_volumes:
          - "/home/Filip/app_projects/app-praktik-projekt/db:/docker-entrypoint-initdb.d:Z"
        container_env_vars:
          POSTGRES_USER: appuser
          POSTGRES_PASSWORD: apppass
          POSTGRES_DB: appdb
        container_state: started
        container_restart_policy: always

    - name: Wait for postgres
      wait_for:
        host: localhost
        port: 5433
        delay: 3
        timeout: 30

    - name: Run backend
      include_role:
        name: containers/run
      vars:
        container_network: app_network
        container_name: rocky_flask_backend_api
        image_name: rocky_flask_backend
        tag: latest
        container_ports:
          - "5000:5000"
        container_env_vars:
          DB_HOST: postgres_database
          DB_USER: appuser
          DB_PASSWORD: apppass
          DB_NAME: appdb
        container_state: started
        container_restart_policy: always

    - name: Run frontend
      include_role:
        name: containers/run
      vars:
        container_network: app_network
        container_name: nginx_frontend
        image_name: nginx
        tag: 1.29.4
        container_ports:
          - "8081:80"
        container_volumes:
          - "/home/Filip/app_projects/app-praktik-projekt/frontend:/usr/share/nginx/html:Z"
        container_cmd:
          - "sh"
          - "-c"
          - "chown -R 0:0 /usr/share/nginx/html && nginx -g 'daemon off;'"
        container_env_vars: {}
        container_state: started
        container_restart_policy: always

Deploy_monitoring.yaml

---
- name: Verify Node Exporter on all VMs
  hosts: all
  roles:
    - role: containers/install
    - role: containers/login/filip
  become: true
  tasks:
    - name: Pull Node Exporter image
      include_role:
        name: containers/images/pull
      vars:
        images_to_pull:
          - manufacturer: prom
            image_name: node-exporter
            tag: latest

    - name: Enable podman socket
      ansible.builtin.systemd:
        name: podman.socket
        state: started
        enabled: true

    - name: Run Node Exporter container
      include_role:
        name: containers/run
      vars:
        container_name: node_exporter
        manufacturer: prom
        image_name: node-exporter
        tag: latest
        container_ports:
          - "9100:9100"
        pid: host
        container_network: host
        container_state: started
        container_restart_policy: always
        container_volumes:
          - /:/host:ro,rslave
        container_cmd:
          - "--path.rootfs=/host"
        container_security_opt: "{{ ['label=disable'] if inventory_hostname in groups['ipaserver'] else omit }}"

- name: Deploy Podman Exporter on Application VM
  hosts: application
  become: true

  roles:
    - role: containers/login/filip
  tasks:
    - name: Pull Podman Exporter image
      include_role:
        name: containers/images/pull
      vars:
        images_to_pull:
          - image_name: prometheus-podman-exporter
            tag: patched

    - name: Enable podman socket
      ansible.builtin.systemd:
        name: podman.socket
        state: started
        enabled: true

    - name: Run Podman Exporter container
      include_role:
        name: containers/run
      vars:
        container_name: podman_exporter
        image_name: prometheus-podman-exporter
        tag: patched
        container_state: started
        container_ports:
          - "9882:9882"
        container_network: host
        container_volumes:
          - "/run/podman/podman.sock:/run/podman/podman.sock:ro"
        container_env_vars:
          CONTAINER_HOST: "unix:///run/podman/podman.sock"
        container_user: root
        container_restart_policy: always

# Prometheus
- name: Deploy Prometheus
  hosts: monitoring
  become: true
  roles:
    - role: containers/login/filip
  tasks:
    - name: Ensure Prometheus config directory exists
      ansible.builtin.file:
        path: /home/Filip/prometheus
        state: directory
        mode: "0755"

    - name: Deploy Prometheus config
      ansible.builtin.copy:
        src: files/prometheus/prometheus.yml
        dest: /home/Filip/prometheus/prometheus.yml
        mode: "0644"

    - name: Run prometheus container
      include_role:
        name: containers/run
      vars:
        container_name: prometheus
        manufacturer: prom
        image_name: prometheus
        tag: main
        container_ports:
          - "9090:9090"
        container_volumes:
          - "/home/Filip/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:Z"
        container_state: started
        container_restart_policy: always
        container_network: host

- name: Deploy Grafana
  hosts: monitoring
  become: true
  tasks:
    - name: Create Grafana provisioning directory
      ansible.builtin.file:
        path: /home/Filip/grafana/provisioning/datasources
        state: directory
        recurse: true
        mode: "0755"

    - name: Configure Grafana datasource
      ansible.builtin.copy:
        src: files/grafana/provisioning/datasources/datasource.yml
        dest: /home/Filip/grafana/provisioning/datasources/datasource.yml
        mode: "0644"

    - name: Run Grafana container
      include_role:
        name: containers/run
      vars:
        container_name: grafana
        image_name: grafana
        tag: "alpine-3.22.2"
        container_ports:
          - "3000:3000"
        container_volumes:
          - "/home/Filip/grafana/provisioning:/etc/grafana/provisioning:Z"
        container_state: started
        container_restart_policy: always
        container_network: host

HTTPS / TLS Configuration

To remove the insecure connection warning in Firefox on the Showcase VM, we configured TLS for both the frontend (Nginx) and backend (Flask) using mkcert-generated certificates.

1. Generate certificates (on Showcase-01)

Using the mkcert binary on Showcase-01:

./mkcert-v1.4.4-linux-amd64 

This generates two files:

  • <IP>.pem — the server certificate
  • <IP>-key.pem — the private key

The rootCA is stored at:

./mkcert-v1.4.4-linux-amd64 -CAROOT
# /home/Filip/.local/share/mkcert/rootCA.pem

2. Store certs on Management-01

Certs are stored under Ansible files on Mgmt-01:

/opt/ansible/files/certificates/app-01-certs/

3. Nginx config

An Nginx config file is stored at /opt/ansible/files/nginx/nginx.conf and copied to App-01 automatically by the playbook:

server {
    listen 443 ssl;
    ssl_certificate     /certs/.pem;
    ssl_certificate_key /certs/-key.pem;
    root /usr/share/nginx/html;
    index index.html;
}

4. Playbook automation

The deploy_app playbook handles everything automatically:

- name: Ensure certs directory exists
  file:
    path: /home/Filip/certs
    state: directory

- name: Copy certs to app-01
  copy:
    src: /opt/ansible/files/certificates/app-01-certs/
    dest: /home/Filip/certs/

- name: Copy nginx config
  copy:
    src: /opt/ansible/files/nginx/nginx.conf
    dest: /home/Filip/app_projects/app-praktik-projekt/nginx.conf

Frontend container mounts:

container_ports:
  - "443:443"
container_volumes:
  - "/home/Filip/app_projects/app-praktik-projekt/frontend:/usr/share/nginx/html:Z"
  - "/home/Filip/certs:/certs:Z"
  - "/home/Filip/app_projects/app-praktik-projekt/nginx.conf:/etc/nginx/conf.d/default.conf:Z"

Backend container mounts:

container_volumes:
  - "/home/Filip/certs:/certs:Z"

5. Flask HTTPS (app.py)

app.run(host="0.0.0.0", port=5000, ssl_context=(
    "/certs/.pem",
    "/certs/-key.pem"
))

6. Trust the certificate in Firefox (Showcase-01)

Import the rootCA into Firefox once so it permanently trusts all mkcert-generated certs:

Firefox → Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import

Import: /home/Filip/.local/share/mkcert/rootCA.pem

Check "Trust this CA to identify websites"

Conclusion

Working on this project as interns gave us a practical look at how modern infrastructure is actually built and maintained. Instead of only working with isolated tools, we had to understand how containers, automation, and monitoring fit together in a real environment. Designing solutions within the constraints of an enterprise setup—such as internal repositories and restricted internet access—also made the work feel closer to real production scenarios.

Beyond the technical result, the project helped us gain confidence in structuring infrastructure through automation and reproducible deployments. Seeing the application stack and monitoring come together across multiple virtual machines was both challenging and rewarding, and it gave us a clearer understanding of the systems that support real-world IT operations.

References

Other projects in our virtual IT-enviroment:

About

Container stack / application Deployment on virtual machines running Podman, through a control vm running ansible

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors