Puppet is a declarative configuration management tool: you describe the state a server should be in, and a Puppet agent on each node pulls its configuration from a central Puppet server and enforces it every 30 minutes. In this tutorial you will install a Puppet server and one agent on Ubuntu 24.04, connect them with signed certificates, and write a small module that installs Nginx and renders a home page from Hiera data.

Since 2025, Perforce no longer publishes new open source Puppet builds in public repositories. This guide uses OpenVox, the community-maintained open source continuation of Puppet from the Vox Pupuli project. It is a drop-in replacement: the commands (puppet, puppetserver, facter), the Puppet language and the file paths are the same, so everything here also applies to Puppet itself.

Prerequisites

To follow this tutorial, you will need:

  • Two servers running Ubuntu 24.04 LTS, for example two CubePath VPS instances:
    • Puppet server: at least 2 vCPUs and 4 GB of RAM, since Puppet Server runs on the JVM. It will be named puppet.example.com.
    • Agent node: 1 GB of RAM is enough. It will be named web1.example.com.
  • A non-root user with sudo privileges on both servers.
  • UFW enabled on both servers with SSH allowed.

Replace puppet.example.com, web1.example.com, your_server_ip and your_agent_ip with your own names and addresses throughout the guide.

Step 1 - Setting hostnames and name resolution

Puppet identifies every node by a TLS certificate issued for its hostname, so the names must be set correctly before installing anything. On the Puppet server, run:

sudo hostnamectl set-hostname puppet.example.com

On the agent node, run:

sudo hostnamectl set-hostname web1.example.com

If you do not have DNS records for these names, add both of them to /etc/hosts on both servers:

sudo nano /etc/hosts
your_server_ip  puppet.example.com  puppet
your_agent_ip   web1.example.com    web1

Verify that each server resolves the other's name and reports its own fully qualified name:

hostname -f
getent hosts puppet.example.com
puppet.example.com
your_server_ip  puppet.example.com puppet

Step 2 - Adding the OpenVox repository

The OpenVox project ships a release package that installs its APT source and signing key. Run these commands on both servers:

wget https://apt.voxpupuli.org/openvox8-release-ubuntu24.04.deb
sudo dpkg -i openvox8-release-ubuntu24.04.deb
sudo apt update

The package adds /etc/apt/sources.list.d/openvox8-release.list, which points to the OpenVox 8 repository with the key in /etc/apt/keyrings/openvox-keyring.gpg. Confirm the server package is available:

apt policy openvox-server
openvox-server:
  Installed: (none)
  Candidate: 8.x.x-1+ubuntu24.04

Step 3 - Installing and starting Puppet Server

On the Puppet server, install the server package. It pulls in the agent and a Java runtime as dependencies:

sudo apt install openvox-server

Puppet Server reserves 2 GB of heap by default. On a 4 GB server that is fine; on a smaller test machine, lower it in /etc/default/puppetserver:

sudo nano /etc/default/puppetserver
JAVA_ARGS="-Xms1g -Xmx1g"

Start the service and enable it at boot. The first start takes a minute because it creates the certificate authority (CA) and the server's own certificate:

sudo systemctl enable --now puppetserver
sudo systemctl status puppetserver
● puppetserver.service - puppetserver Service
     Loaded: loaded (/usr/lib/systemd/system/puppetserver.service; enabled; preset: enabled)
     Active: active (running)

The Puppet binaries live in /opt/puppetlabs/bin. A profile script adds this directory to your PATH at the next login, but sudo does not inherit it, so this guide uses full paths. Confirm that the CA issued the server certificate:

sudo /opt/puppetlabs/bin/puppetserver ca list --all
Signed Certificates:
    puppet.example.com       (SHA256)  9A:3F:...:C1

Agents connect to the server on TCP port 8140. Allow it from the agent:

sudo ufw allow from your_agent_ip to any port 8140 proto tcp

Step 4 - Installing the agent and requesting a certificate

On the agent node, install the agent package:

sudo apt install openvox-agent

Point the agent at your server. This writes the server setting into /etc/puppetlabs/puppet/puppet.conf:

sudo /opt/puppetlabs/bin/puppet config set server puppet.example.com --section main

Run the agent once in the foreground. It generates a key, sends a certificate signing request (CSR) to the server and stops, because the certificate is not signed yet:

sudo /opt/puppetlabs/bin/puppet agent --test
Info: Creating a new RSA SSL key for web1.example.com
Info: Certificate Request fingerprint (SHA256): 4B:12:...:7E
Info: Certificate for web1.example.com has not been signed yet
Couldn't fetch certificate from CA server; you might still need to sign this agent's certificate (web1.example.com).
Exiting now because the waitforcert setting is set to 0.

Take note of the fingerprint.

Step 5 - Signing the agent certificate

On the Puppet server, list pending requests:

sudo /opt/puppetlabs/bin/puppetserver ca list
Requested Certificates:
    web1.example.com       (SHA256)  4B:12:...:7E

If the fingerprint matches the one printed on the agent, sign the request:

sudo /opt/puppetlabs/bin/puppetserver ca sign --certname web1.example.com
Successfully signed certificate request for web1.example.com

Back on the agent node, run the agent again. This time it downloads its certificate and applies the catalog, which is still empty:

sudo /opt/puppetlabs/bin/puppet agent --test
Info: Using environment 'production'
Info: Caching catalog for web1.example.com
Info: Applying configuration version '1758794521'
Notice: Applied catalog in 0.01 seconds

Step 6 - Creating a module for Nginx

Puppet code is organized in modules inside an environment. The default environment is production, located at /etc/puppetlabs/code/environments/production. Work on the Puppet server for the rest of the configuration.

Create the module directory structure:

sudo mkdir -p /etc/puppetlabs/code/environments/production/modules/nginx_site/{manifests,templates}

Create the main class of the module:

sudo nano /etc/puppetlabs/code/environments/production/modules/nginx_site/manifests/init.pp
class nginx_site (
  String $site_title = 'Hello from Puppet',
) {
  package { 'nginx':
    ensure => installed,
  }

  file { '/var/www/html/index.html':
    ensure  => file,
    owner   => 'root',
    group   => 'root',
    mode    => '0644',
    content => epp('nginx_site/index.html.epp', { 'title' => $site_title }),
    require => Package['nginx'],
  }

  service { 'nginx':
    ensure  => running,
    enable  => true,
    require => Package['nginx'],
  }
}

The class declares three resources: the package, the home page and the service. require sets their order, and $site_title is a class parameter that Hiera will fill in.

Create the EPP template for the page. The first line declares the parameters it accepts, and $facts gives access to facts that Facter collected on the agent:

sudo nano /etc/puppetlabs/code/environments/production/modules/nginx_site/templates/index.html.epp
<%- | String $title | -%>
<!DOCTYPE html>
<html>
  <head><title><%= $title %></title></head>
  <body>
    <h1><%= $title %></h1>
    <p>Served by <%= $facts['networking']['fqdn'] %> running <%= $facts['os']['name'] %> <%= $facts['os']['release']['full'] %>.</p>
  </body>
</html>

Check the syntax of the class:

sudo /opt/puppetlabs/bin/puppet parser validate /etc/puppetlabs/code/environments/production/modules/nginx_site/manifests/init.pp

No output means the manifest is valid.

Step 7 - Adding Hiera data and assigning the class

Hiera keeps data out of your code. When Puppet declares nginx_site, it automatically looks up the key nginx_site::site_title and uses it for the $site_title parameter.

Create the environment's Hiera configuration:

sudo nano /etc/puppetlabs/code/environments/production/hiera.yaml
---
version: 5
defaults:
  datadir: data
  data_hash: yaml_data
hierarchy:
  - name: "Per-node data"
    path: "nodes/%{trusted.certname}.yaml"
  - name: "Common data"
    path: "common.yaml"

Hiera checks a node-specific file first and falls back to common.yaml. Create the common data file:

sudo mkdir -p /etc/puppetlabs/code/environments/production/data/nodes
sudo nano /etc/puppetlabs/code/environments/production/data/common.yaml
---
nginx_site::site_title: 'Managed by Puppet'

Now tell Puppet which node gets the class. Node definitions go in the main manifest, site.pp:

sudo nano /etc/puppetlabs/code/environments/production/manifests/site.pp
node 'web1.example.com' {
  include nginx_site
}

node default {
}

Verify that Hiera returns the value for this node:

sudo /opt/puppetlabs/bin/puppet lookup nginx_site::site_title --node web1.example.com --environment production
--- Managed by Puppet

Step 8 - Applying the configuration on the agent

On the agent node, do a dry run first. With --noop, Puppet shows what would change without changing it:

sudo /opt/puppetlabs/bin/puppet agent --test --noop
Notice: /Stage[main]/Nginx_site/Package[nginx]/ensure: current_value 'purged', should be 'present' (noop)
Notice: Applied catalog in 0.42 seconds

Apply the catalog:

sudo /opt/puppetlabs/bin/puppet agent --test
Notice: /Stage[main]/Nginx_site/Package[nginx]/ensure: created
Notice: /Stage[main]/Nginx_site/File[/var/www/html/index.html]/content: content changed
Notice: Applied catalog in 12.84 seconds

Allow HTTP on the agent and request the page:

sudo ufw allow 'Nginx HTTP'
curl http://localhost
<!DOCTYPE html>
<html>
  <head><title>Managed by Puppet</title></head>
  <body>
    <h1>Managed by Puppet</h1>
    <p>Served by web1.example.com running Ubuntu 24.04.</p>
  </body>
</html>

Finally, enable the agent service so it checks in every 30 minutes and corrects any drift on its own:

sudo systemctl enable --now puppet

To test drift correction, edit /var/www/html/index.html by hand and run sudo /opt/puppetlabs/bin/puppet agent --test again: Puppet restores the managed content.

Troubleshooting

  • Could not request certificate: Failed to open TCP connection to puppet.example.com:8140: the agent cannot reach the server. Check the /etc/hosts entry and the UFW rule on the server, and run nc -zv puppet.example.com 8140 from the agent.
  • Server hostname 'puppet.example.com' did not match server certificate: the server's certificate was created before you set its hostname. On a fresh server with no agents yet, stop puppetserver, set the hostname as in Step 1, delete /etc/puppetlabs/puppetserver/ca and /etc/puppetlabs/puppet/ssl, and start the service again so it issues a new CA and certificate.
  • An agent was rebuilt and now fails with a certificate mismatch: revoke the old certificate on the server with sudo /opt/puppetlabs/bin/puppetserver ca clean --certname web1.example.com, delete /etc/puppetlabs/puppet/ssl on the agent, and request a new certificate.
  • puppetserver fails to start: check sudo journalctl -u puppetserver -n 50. An out of memory error means JAVA_ARGS is too large for the server's RAM.

Conclusion

You installed a Puppet server and agent on Ubuntu 24.04 with OpenVox, signed the agent's certificate, and deployed Nginx through a module that takes its data from Hiera. Next, you can keep /etc/puppetlabs/code in Git and deploy it with r10k, install community modules from the Puppet Forge with puppet module install, or add per-node overrides in data/nodes/web1.example.com.yaml.