Skip to main content

Podman Play to Deploy Any App

· 10 min read
VoidQuark
Open-source enthusiast

Podman Play to deploy any app

Deploy Nextcloud, Hashi Vault, Dashy and Jellyfin in simple way with this Ansible guide. In this blog post, we will explore how to use 🦭 Podman play Ansible Role and deploy a popular application in root-less containers from a Kubernetes Pod YAML definition. The application pod runs as a quadlet systemd service in your own user namespace.

Using Ansible roles has several benefits. One of them is that you can easily reproduce the same deployment with inventory variables. This means that you can manage your application without having to run manual commands. With Ansible, you have complete control over your application.

If you’re not familiar with Ansible, you can read an introduction here.

One Role To Deploy Multiple Applications

This role has been created for a simple yet powerful purpose: to deploy multiple applications using a single role. By doing this, we eliminate the need to create a separate role for each application. Instead, we simply create an inventory and a straightforward playbook that calls this role with different variables for each application. In this guide, we'll focus on deploying Nextcloud, HashiCorp Vault, Dashy, and Jellyfin, but you can easily extend this approach to deploy any other application you like.

About Podman Play Kube 🦭

The choice of the podman_play module over alternatives like podman_container or podman_pod was deliberate. This decision was made to create a role that can be easily shared with others seeking a similar solution, satisfying the following criteria:

  • Simplified Deployment: Deploy your applications with ease using standard Kubernetes YAML definitions.
  • No Need for Role Development per Application
  • Simple Variables
  • Systemd Unit: Control applications with Quadlet Systemd units, ensuring they start on boot.
  • Support Auto Update: If you prefer using latest or stable tags, this role allows you to utilize Podman's auto-update feature, ensuring automatic updates of your pods.

These simple criteria should help you test new applications or deploy them in a simple and reproducible way.

Let's Deploy Applications

Before delving into the specific application deployments mentioned below, it's recommended to read the entire README of the role for a comprehensive understanding of its variables.

Prepare Requirements

Before we begin the deployment, let's ensure that necessary requirements are properly set up.

All applications will be deployed on a single VM. In this guide, variables are stored directly within each playbook rather than in the inventory, for simplicity. However, you can adjust this setup to use a more traditional inventory structure with group_vars and host_vars for shared configurations, depending on your specific scenario and use case. This approach makes the guide easier to read and understand.

Required Playbooks
function_nextcloud_deploy.yml # Playbook to deploy Nextcloud
function_vault_deploy.yml # Playbook to deploy Hashi Vault
function_dashy_deploy.yml # Playbook to deploy Dashy
function_jellyfin_deploy.yml # Playbook to deploy Jellyfin

It is assumed that you have fulfilled the role README requirements section, such as having the podman and posix collections installed. Additionally, ensure you have installed the voidquark.podman_play role with the following command:

Install voidquark.podman_play
ansible-galaxy install voidquark.podman_play

Deploy Nextcloud

Let's dive straight into deployment. Create a simple playbook to deploy Nextcloud containers, which will run in a single pod.

Playbook - function_nextcloud_deploy.yml
- name: Deploy Nextcloud service
hosts: nextcloud
become: true
gather_facts: true
vars:
podman_play_pod_name: "nextcloud"
podman_play_user: "nextcloud"
podman_play_group: "nextcloud"
podman_play_auto_update: true
podman_play_firewalld_expose_ports:
- "9520/tcp"
podman_play_dirs:
- "{{ podman_play_root_dir }}/var_www_html"
- "{{ podman_play_root_dir }}/data"
- "{{ podman_play_root_dir }}/var_lib_mysql"
- "{{ podman_play_root_dir }}/config"
podman_play_pod_yaml_definition: |
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: "{{ podman_play_pod_name }}"
name: "{{ podman_play_pod_name }}"
annotations:
io.containers.autoupdate: registry
spec:
containers:
- name: nextcloud
image: docker.io/nextcloud:production-apache
ports:
- containerPort: 80
hostPort: 9520
stdin: true
tty: true
volumeMounts:
- mountPath: /var/www/html:Z
name: var_www_html
- mountPath: /var/www/html/data:Z
name: data
- mountPath: /var/www/html/config:Z
name: config
env:
- name: MYSQL_DATABASE
value: "nextcloud"
- name: MYSQL_USER
value: "nextcloud"
- name: MYSQL_PASSWORD
value: "dummypassword"
- name: MYSQL_HOST
value: "127.0.0.1"
- name: NEXTCLOUD_ADMIN_USER
value: "admin"
- name: NEXTCLOUD_ADMIN_PASSWORD
value: "dummypassword"
- name: NEXTCLOUD_DATA_DIR
value: "/var/www/html/data"
- name: mariadb
image: docker.io/mariadb:10.6
stdin: true
tty: true
volumeMounts:
- mountPath: /var/lib/mysql:Z
name: var_www_mysql
env:
- name: MYSQL_DATABASE
value: "nextcloud"
- name: MYSQL_USER
value: "nextcloud"
- name: MYSQL_PASSWORD
value: "dummypassword"
- name: MYSQL_ROOT_PASSWORD
value: "dummypassword"
volumes:
- hostPath:
path: "{{ podman_play_root_dir }}/var_www_html"
type: Directory
name: var_www_html
- hostPath:
path: "{{ podman_play_root_dir }}/data"
type: Directory
name: data
- hostPath:
path: "{{ podman_play_root_dir }}/config"
type: Directory
name: config
- hostPath:
path: "{{ podman_play_root_dir }}/var_lib_mysql"
type: Directory
name: var_www_mysql
roles:
- voidquark.podman_play

The above playbook deploys Nextcloud and MariaDB containers in the nextcloud pod. This deployment is for development purposes only and excludes additional environment variables and configuration adjustments. It also does not include the Redis container. The purpose is to showcase role deployment with simplified configuration, avoiding unnecessary complexity. I've also incorporated a feature into this role that enables automatic updates for the Nextcloud pod. This ensures that this small development instance will receive updates to the Nextcloud image as soon as the production-apache tag is updated on Docker Hub with a newer version.

Execute the playbook, and once the deployment is finished, you can access Nextcloud at http://yourInstance:9520.

Deploy Hashi Vault

Next in line is Hashi Vault, known for its robust configuration capabilities. For our scenario, deploying a development instance will suffice. I also enabled the auto-update feature for this pod. Let's kick off the deployment.

Playbook - function_vault_deploy.yml
- name: Deploy Hashi Vault service
hosts: vault
become: true
gather_facts: true
vars:
podman_play_pod_name: "vault"
podman_play_user: "hashivault"
podman_play_group: "hashivault"
podman_play_auto_update: true
podman_play_firewalld_expose_ports:
- "9521/tcp"
podman_play_pod_yaml_definition: |
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: "{{ podman_play_pod_name }}"
name: "{{ podman_play_pod_name }}"
annotations:
io.containers.autoupdate: registry
spec:
containers:
- name: "{{ podman_play_pod_name }}"
image: docker.io/hashicorp/vault:latest
ports:
- containerPort: 8200
hostPort: 9521
stdin: true
env:
- name: VAULT_DEV_ROOT_TOKEN_ID
value: "dontUseThisToken"
roles:
- voidquark.podman_play

Execute the playbook, and once the deployment is finished, the development Hashi Vault is accessible at http://yourInstance:9521.

Deploy Dashy

To highlight the flexibility of this role and showcase that root privileges are not required, I have chosen to deploy the Dashy application on a workstation.

Playbook - function_dashy_deploy.yml
- name: Deploy Dashy service
hosts: dashy
connection: local
gather_facts: true
vars:
podman_play_pod_name: "dashy"
podman_play_pod_yaml_definition: |
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: "{{ podman_play_pod_name }}"
name: "{{ podman_play_pod_name }}"
spec:
containers:
- name: "{{ podman_play_pod_name }}"
image: docker.io/lissy93/dashy:latest
ports:
- containerPort: 80
hostPort: 9522
stdin: true
tty: true
volumeMounts:
- mountPath: /app/public/conf.yml:Z
name: dashy_config
volumes:
- hostPath:
path: "{{ podman_play_template_config_dir }}/conf.yml"
type: File
name: dashy_config
podman_play_custom_conf:
- filename: "conf.yml"
raw_content: |
pageInfo:
title: Dashy Services
description: Example Dashy Dashboard
navLinks:
- title: Dashy docs
path: https://dashy.to/docs/
footerText: "This is footer text"
# Optional app settings and configuration
appConfig:
theme: default
# Main content - An array of sections, each containing an array of items
sections:
- name: VoidQuark - Services
icon: ':rocket:'
items:
- title: VoidQuark
description: VoidQuark Web
icon: favicon
url: https://voidquark.com
- title: Prometheus
description: Prometheus
icon: hl-prometheus
url: https://prometheus.io/
roles:
- voidquark.podman_play

Execute the playbook, and once the deployment is finished, the Dashy service is accessible at http://localhost:9522.

Deploy Jellyfin

Let's deploy a fresh Jellyfin instance that will be automatically updated, allowing you to focus on enjoying your favorite movies and shows.

Playbook - function_jellyfin_deploy.yml
- name: Deploy Jellyfin service
hosts: jellyfin
become: true
gather_facts: true
vars:
podman_play_pod_name: "jellyfin-pod"
podman_play_user: "jellyfin"
podman_play_group: "jellyfin"
podman_play_auto_update: true
podman_play_firewalld_expose_ports:
- "8096/tcp"
podman_play_dirs:
- "/mnt/jellyfin_data/jellyfin-cache"
- "/mnt/jellyfin_data/jellyfin-config"
- "/mnt/jellyfin_data/jellyfin-media"
podman_play_pod_yaml_definition: |
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: "{{ podman_play_pod_name }}"
name: "{{ podman_play_pod_name }}"
annotations:
io.containers.autoupdate: registry
spec:
containers:
- name: "{{ podman_play_pod_name }}"
image: docker.io/jellyfin/jellyfin:latest
ports:
- containerPort: 8096
hostPort: 8096
stdin: true
tty: true
volumeMounts:
- mountPath: /config:Z
name: jellyfin_config
- mountPath: /cache:Z
name: jellyfin_cache
- mountPath: /media:Z
name: jellyfin_media
volumes:
- hostPath:
path: "/mnt/jellyfin_data/jellyfin-config"
type: Directory
name: jellyfin_config
- hostPath:
path: "/mnt/jellyfin_data/jellyfin-cache"
type: Directory
name: jellyfin_cache
- hostPath:
path: "/mnt/jellyfin_data/jellyfin-media"
type: Directory
name: jellyfin_media
roles:
- voidquark.podman_play

Execute the playbook, and once the deployment is finished, instance will be available at http://yourInstance:8096. After completing the initial setup and uploading your media, you can start enjoying movies and shows.

Auto Update Feature

In some cases, you may simply want to run an application in a pod and leave it there without triggering updates frequently, especially if the application is straightforward and rarely undergoes breaking changes. Enabling auto-update makes sense in such scenarios.

This role utilizes podman auto-update. Once you set the variable podman_play_auto_update: true, the role enables and starts podman-auto-update.timer. However, to update the pod, we need to add the pod annotation io.containers.autoupdate: registry. By default, once per day, podman-auto-update.timer triggers podman-auto-update.service, ensuring that your latest tag image is updated whenever a new version becomes available in your registry, such as Docker Hub.

This approach maintains idempotence in your configuration because your inventory still matches your deployment. The pod uses a non-version-specific tag, and the entire configuration remains the same. Only the systemd unit is stopped and started, and the pod is recreated with the new latest image. As you can see, it's a straightforward and non-magical 🪄 process.

Extend Deployment Playbook and Cleanup

Customizing the deployment process is made easy by leveraging pre_tasks and post_tasks in your playbook, allowing for additional modifications without altering the Ansible role. To get some idea what you can achieve with pre_tasks or post_tasks then you can read more: Set the order of tasks execution in Ansible with these two keywords.

As of now, the role does not natively support cleanup or uninstallation. Future updates may include this capability. When removing your containers, ensure that you also clean up /home/USER/.config/systemd/user directory as systemd unit files are stored here. Additionally, for application data, it is recommended to clean up the /home/USER/POD_NAME directory.

Conclusion

I believe this role offers significant benefits by simplifying the deployment process. It's important to note that while it may not be suitable for every solution, it provides a streamlined approach for smaller deployments where using the Kubernetes platform might be overkill.

This role is designed for scenarios where a straightforward pod deployment is sufficient, yet having source control in place and avoiding ad-hoc commands like podman run. It has proven useful in various scenarios for me, and I find it valuable to share with the community, as it may meet the needs of others seeking a similar solution.

If you're interested in how this actually works in details, I recommend checking out the documentation below:

Read more about Podman


Thanks for reading. I'm entering the void. 🛸 ➡️ 🕳️