I spent a weekend diving into Podman Quadlets, exploring how to deploy Temporal on a Linux machine using this relatively new container management approach. While Temporal’s website has excellent docs for self-hosting, I found a gap when it comes to Podman Quadlets. Since I’ve been working more with edge deployments lately, I thought this would be a perfect opportunity to bridge that gap.
What You’ll Learn
In this tutorial, we’ll cover:
- Deploying multi-container applications with Podman Quadlets
- Creating systemd services from containers for production-like management
- Configuring an nginx reverse proxy with SSL
- Setting up Temporal for local development with PostgreSQL (Although better use just use temporal cli for local dev)
By the end, you’ll have a fully functional Temporal deployment that’s managed by systemd, making it ideal for production environments, especially those with high security requirements.
Source Code
All the code for this tutorial is available on GitHub: temporal-quadlets
You can clone the repository and follow along:
git clone https://github.com/smaldd14/temporal-quadlets.git
cd temporal-quadlets
Prerequisites
- A RHEL 10 machine (or similar with Podman 4.4+)
- Basic familiarity with containers and systemd
- Linux/bash command line skills
- SSH access to your target machine
I initially wanted to deploy this on my Raspberry Pi, but discovered it was running Debian Bookworm with only Podman 4.3.1. Since Quadlets require Podman 4.4+, I set up a RHEL 10 VM to ensure we’re working with the latest features.
Technology Overview
Temporal: Workflow Orchestration Made Simple
Temporal is a workflow orchestration platform that makes building reliable, scalable applications significantly easier. Instead of dealing with the complexities of distributed systems, timeouts, retries, and state management yourself, Temporal handles these concerns for you.
For our deployment, we’re setting up three core Temporal components:
- Temporal Server: The core workflow engine that orchestrates your workflows
- Temporal UI: A web interface for monitoring workflows, debugging, and operational visibility
- PostgreSQL: The database backend for persistence and state management
Podman Quadlets: Containers Meet systemd
Podman Quadlets represent a fundamental shift in how we deploy containers in production. Rather than relying on Docker’s daemon architecture, Quadlets integrate directly with systemd, offering several compelling advantages:
What are Quadlets? They’re configuration files that describe containers, networks, and volumes in a systemd-native way. Podman reads these .container, .network, and .volume files and automatically generates corresponding systemd services.
Why they matter for production:
- No daemon dependency: Unlike Docker, there’s no central daemon that could become a single point of failure
- Native systemd integration: Automatic restarts, dependency management, and logging work out of the box (think journalctl)
- Security by default: Each container runs in its own systemd slice with proper isolation
Edge and High-Security Benefits
This approach is particularly valuable for edge systems and high-security environments:
- Reduced attack surface: No container daemon running as root
- Better compliance: systemd’s security features and audit trails
- Simplified operations: Standard systemd tools for monitoring and management
- Rootless capabilities: Can run entirely without root privileges when needed (I initially tried this, but ran into some quirks that I will explain later)
Architecture Overview
Our deployment consists of four main components connected through a custom network:
[Architecture diagram will be added here]
- PostgreSQL: Provides persistent storage for Temporal’s state
- Temporal Server: The core engine that executes workflows
- Temporal UI: Web interface accessible on port 8080
- Nginx: Reverse proxy providing external access and SSL termination
All components communicate over a dedicated temporal-network, allowing them to reach each other by container name while remaining isolated from other services.
Implementation
Step 1: Project Structure
Our Podman Quadlets are organized in a clear directory structure:
services/temporal/
├── temporal-postgres.container # PostgreSQL database
├── temporal-server.container # Temporal workflow engine
├── temporal-ui.container # Web UI
├── temporal-nginx.container # Reverse proxy
├── temporal.network # Custom network
├── temporal-config.volume # Configuration volume
├── nginx-config.volume # Nginx config volume
├── nginx-ssl.volume # SSL certificates volume
└── config/
├── temporal/
│ └── development-sql.yaml # Temporal dynamic config
└── nginx/
├── conf.d/
│ └── temporal.conf # Nginx site config
└── ssl/
├── temporal.crt # SSL certificate
└── temporal.key # SSL private key
Step 2: Database Foundation
PostgreSQL serves as our persistence layer. Here’s the quadlet configuration:
[Unit]
Description=PostgreSQL Database for Temporal
[Container]
Image=docker.io/library/postgres:16
ContainerName=temporal-postgresql
# PostgreSQL configuration
Environment=POSTGRES_PASSWORD=temporal
Environment=POSTGRES_USER=temporal
Environment=POSTGRES_DB=temporal
# Persistent data storage using named volume
Volume=postgres-data:/var/lib/postgresql/data
Network=temporal-network
# Keep port published for external access if needed
PublishPort=5432:5432
# Health check
HealthCmd=pg_isready -U temporal
HealthInterval=10s
HealthTimeout=5s
HealthRetries=5
[Service]
Restart=always
TimeoutStartSec=60
[Install]
WantedBy=multi-user.target
📁 Complete file:
services/temporal/temporal-postgres.container
Key points:
- Uses a named volume for data persistence across container restarts
- Connects to our custom network for service discovery
- Includes health checks to ensure PostgreSQL is ready before dependent services start
Step 3: Temporal Server Configuration
The Temporal Server is the heart of our deployment. It connects to PostgreSQL and provides the workflow execution engine:
[Unit]
Description=Temporal Server
Requires=temporal-postgres.service
After=temporal-postgres.service
[Container]
Image=docker.io/temporalio/auto-setup:1.28.1
ContainerName=temporal-server
Network=temporal-network
PublishPort=7233:7233
# PostgreSQL configuration
Environment=DB=postgres12
Environment=DB_PORT=5432
Environment=POSTGRES_USER=temporal
Environment=POSTGRES_PWD=temporal
Environment=POSTGRES_SEEDS=temporal-postgresql
Environment=POSTGRES_DB=temporal
# Volume mounts for dynamic configuration
Volume=temporal-config-volume:/etc/temporal/config/dynamicconfig
# Health check
HealthCmd=temporal --address localhost:7233 operator cluster health
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=multi-user.target
📁 Complete file:
services/temporal/temporal-server.container
The auto-setup image automatically creates the necessary database schemas on startup, simplifying deployment. The server connects to PostgreSQL using the container name temporal-postgresql thanks to our custom network.
In a production setup, especially with security in mind, you would probably want to use the dedicated temporal-server image, and most likely want to build off a trusted base image rather than using auto-setup in production.
Step 4: UI Setup
The Temporal UI provides essential operational visibility:
[Unit]
Description=Temporal UI
Requires=temporal-server.service
After=temporal-server.service
[Container]
Image=docker.io/temporalio/ui:2.39.0
ContainerName=temporal-ui
Network=temporal-network
# Connect to temporal server
Environment=TEMPORAL_ADDRESS=temporal-server:7233
Environment=TEMPORAL_CORS_ORIGINS=http://localhost:8080
# UI configuration
Environment=TEMPORAL_UI_PORT=8080
[Service]
Restart=always
TimeoutStartSec=60
[Install]
WantedBy=multi-user.target
📁 Complete file:
services/temporal/temporal-ui.container
The UI connects directly to the Temporal Server and serves the web interface on port 8080. In our architecture, nginx will proxy requests to this internal port.
Step 5: Nginx Reverse Proxy
Nginx provides external access with SSL termination. We use separate volumes for configuration and SSL certificates, following security best practices:
[Unit]
Description=Nginx Reverse Proxy for Temporal
Requires=temporal-ui.service
After=temporal-ui.service
[Container]
Image=docker.io/library/nginx:1.27-alpine
ContainerName=temporal-nginx
Network=temporal-network
# Volume mounts for configuration and certificates
Volume=systemd-nginx-config:/etc/nginx/conf.d:ro,Z
Volume=systemd-nginx-ssl:/etc/nginx/ssl:ro,Z
# Expose HTTP and HTTPS ports
PublishPort=80:80
PublishPort=443:443
# Health check
HealthCmd=curl -f http://temporal-server:7233/health || exit 1
HealthInterval=30s
HealthTimeout=5s
HealthRetries=3
[Service]
Restart=always
TimeoutStartSec=60
[Install]
WantedBy=multi-user.target
📁 Complete file:
services/temporal/temporal-nginx.container
The nginx configuration proxies requests to the Temporal UI while providing SSL encryption for external access.
Step 6: Network and Volume Management
Our custom network enables service discovery:
[Network]
NetworkName=temporal-network
Driver=bridge
Subnet=172.20.0.0/16
Gateway=172.20.0.1
[Install]
WantedBy=multi-user.target
📁 Complete file:
services/temporal/temporal.network
Volume quadlets manage our configuration and SSL certificates. Here are the key volume definitions:
Nginx Configuration Volume:
[Unit]
Description=Nginx Configuration and Certificates Volume
[Volume]
Driver=local
Device=/opt/temporal/config/nginx/conf.d
Type=bind
Options=bind
[Install]
WantedBy=multi-user.target
SSL Certificates Volume:
[Unit]
Description=Nginx SSL Certificates Volume
[Volume]
Driver=local
Device=/opt/temporal/config/nginx/ssl
Type=bind
Options=bind
[Install]
WantedBy=multi-user.target
📁 Complete files:
nginx-config.volumeandnginx-ssl.volume
This separation follows security best practices:
- Configuration files are stored in
/opt/temporal/config/nginx/conf.d - SSL certificates are isolated in
/opt/temporal/config/nginx/ssl
Deployment Steps
1. Prepare Your Environment
SSH into your RHEL machine and switch to root for system-wide deployment:
ssh your-vm
sudo su -
2. Create Directory Structure
Set up the configuration directories where our volumes will mount:
mkdir -p /opt/temporal/config/{temporal,nginx/{conf.d,ssl}}
3. Copy Files
Transfer your quadlet files and configurations:
# Copy quadlet definitions
scp services/temporal/*.container services/temporal/*.volume services/temporal/*.network root@{VM_IP}:/etc/containers/systemd/
# Copy configuration files
scp services/temporal/config/temporal/development-sql.yaml vbvm:/opt/temporal/config/temporal/
scp services/temporal/config/nginx/conf.d/temporal.conf vbvm:/opt/temporal/config/nginx/conf.d/
scp services/temporal/config/nginx/ssl/* vbvm:/opt/temporal/config/nginx/ssl/
# Set proper ownership
chown -R root:root /opt/temporal/config/
4. Deploy Services
Reload systemd to recognize the new quadlets, then start services in dependency order:
# Make systemd aware of our new services
systemctl daemon-reload
# Verify our services are loaded
systemctl list-unit-files | grep temporal
# Start services in dependency order
systemctl start temporal.network
systemctl start temporal-postgres.service
# Wait for PostgreSQL to be ready
systemctl start temporal-server.service
# tail logs
journalctl -f -u temporal-server.service
# Wait for server initialization
systemctl start temporal-ui.service
systemctl start temporal-nginx.service
Because the systemd files are generated by Podman by being in /etc/containers/systemd, there is no need to systemctl enable them.
5. Verify Deployment
Check that all services are running correctly:
# Check service status
systemctl status temporal*.service
# Monitor Temporal Server logs to ensure it connected to PostgreSQL
journalctl -f -u temporal-server.service
# Test nginx health check
curl http://localhost/health
Accessing Your Temporal Deployment
Once deployed, you can access Temporal through multiple endpoints:
- Local access:
http://localhost:8080(direct to UI) - Via nginx:
http://localhost:80orhttps://localhost:443, or justlocalhostin your browser in the VM
VirtualBox Port Forwarding (Optional)
If you’re running this in a VM and want to access it from your host machine, set up port forwarding in VirtualBox:
- Go to VM Settings → Network → Advanced → Port Forwarding
- Add these rules:
- SSH: Host Port 2222 → Guest Port 22
- Temporal HTTP: Host Port 8080 → Guest Port 80
- Temporal HTTPS: Host Port 8443 → Guest Port 443
- Temporal UI Direct: Host Port 9080 → Guest Port 8080
Then access from your host machine:
- HTTP via nginx:
http://localhost:8080 - HTTPS via nginx:
https://localhost:8443(will show certificate warning) - Direct UI:
http://localhost:9080
Troubleshooting
Common Issues
Temporal Server waiting for PostgreSQL: Check that PostgreSQL is running and accessible:
systemctl status temporal-postgres.service
podman exec temporal-postgresql pg_isready -U temporal
Permission errors with nginx volumes: Ensure proper ownership of configuration directories:
chown -R root:root /opt/temporal/config/
Services not starting: Check systemd service status and logs:
systemctl status service-name.service
journalctl -u service-name.service -n 50
Useful Debugging Commands
# Watch all Temporal services in real-time
journalctl -f -u temporal-postgres.service -u temporal-server.service -u temporal-ui.service -u temporal-nginx.service
# Check container logs directly
podman logs temporal-server
podman logs temporal-ui
# Verify network connectivity
podman network inspect temporal-network
Production Considerations
This setup uses self-signed certificates for local development. For production deployment with real domains and Let’s Encrypt certificates, see the Temporal nginx guide.
Additional production considerations:
- Implement proper backup strategies for PostgreSQL data
- Configure log rotation and monitoring
- Set up proper firewall rules
- Consider using separate machines for different components
- Implement proper secrets management instead of hardcoded passwords
Conclusion
Podman Quadlets offers a great alternative to traditional container orchestration, especially for edge deployments and high-security environments. By integrating directly with systemd, we get production-grade container management without the complexity and security concerns of daemon-based solutions, like docker/kubernetes.
This Temporal deployment demonstrates how Quadlets can manage complex multi-container applications with proper networking, volume management, and service dependencies. The result is a robust, systemd-native deployment that’s perfect for production environments where reliability and security are high priority.
The next step would be to start building and deploying Temporal workflows to take advantage of this solid foundation. The combination of Temporal’s workflow capabilities with Podman’s security-focused approach creates an excellent platform for building reliable distributed applications. Stay tuned for more tutorials on using Temporal effectively!