Chef is a configuration management platform in which you describe servers in Ruby-based recipes, grouped into cookbooks. You write and test cookbooks on a workstation, upload them to the Chef Infra Server, and the Chef Infra Client on each node downloads its run list from the server and converges the machine to the declared state. In this tutorial you will build all three pieces: install Chef Infra Server, configure Chef Workstation and knife, write a cookbook that deploys Nginx, and bootstrap an Ubuntu 24.04 node that runs it on a schedule.

Prerequisites

To follow this tutorial, you will need three machines:

  • Chef Infra Server: a server with at least 4 vCPUs and 8 GB of RAM, with a fully qualified hostname such as chef.example.com. Progress publishes Chef Infra Server packages for Ubuntu 22.04 LTS, so use that release for this machine.
  • Workstation: your Linux laptop or an Ubuntu 24.04 server, where you write cookbooks and run knife.
  • Node: an Ubuntu 24.04 LTS server to be managed, reachable over SSH from the workstation with a key, as a user with passwordless sudo. Its address is referred to as your_node_ip.

CubePath VPS instances work well for the server and the node. You also need:

  • A DNS A record for chef.example.com pointing to the Chef server, or an /etc/hosts entry for it on the workstation and on the node.
  • UFW enabled on the servers with SSH allowed.

Step 1 - Installing Chef Infra Server

Log in to the Chef server. Make sure its hostname is the fully qualified name that clients will use, because the server's TLS certificate is generated for it:

sudo hostnamectl set-hostname chef.example.com
hostname -f
chef.example.com

Chef distributes its packages through the Omnitruck installer script. Download it and read it before running it:

curl -fsSL https://omnitruck.chef.io/install.sh -o install.sh
less install.sh

Install the latest stable Chef Infra Server package:

sudo bash install.sh -c stable -P chef-server

Run the initial configuration. This sets up all bundled services (PostgreSQL, OpenSearch, the API and the web front end) and takes several minutes:

sudo chef-server-ctl reconfigure --chef-license=accept
Chef Infra Server Reconfigured!

Check that every service is running:

sudo chef-server-ctl status
run: bookshelf: (pid 5021) 95s; run: log: (pid 4870) 110s
run: nginx: (pid 5230) 90s; run: log: (pid 4990) 105s
run: oc_bifrost: (pid 4812) 118s; run: log: (pid 4801) 120s
run: oc_id: (pid 4920) 100s; run: log: (pid 4911) 102s
run: opensearch: (pid 4600) 130s; run: log: (pid 4590) 132s
run: opscode-erchef: (pid 5110) 92s; run: log: (pid 5100) 94s
run: postgresql: (pid 4500) 140s; run: log: (pid 4490) 142s
run: redis_lb: (pid 4700) 125s; run: log: (pid 4690) 127s

Workstations and nodes talk to the server over HTTPS. Open port 443:

sudo ufw allow 443/tcp

Step 2 - Creating an administrator and an organization

Chef Infra Server is multi-tenant: every node and cookbook belongs to an organization, and users authenticate with RSA keys instead of passwords. Still on the Chef server, create an administrator. The arguments are the user name, first name, last name, email and password; replace them with your own values and use a strong password:

sudo chef-server-ctl user-create your_user Your Name [email protected] 'your_strong_password' --filename "$HOME/your_user.pem"

Create an organization and make that user its administrator. The short name (cubelab here) becomes part of the server URL:

sudo chef-server-ctl org-create cubelab 'CubeLab Infrastructure' --association_user your_user --filename "$HOME/cubelab-validator.pem"

The user's private key was written by root, so give it to your account so you can copy it to the workstation:

sudo chown "$USER": "$HOME/your_user.pem"
chmod 600 "$HOME/your_user.pem"

Verify both objects exist:

sudo chef-server-ctl user-list
sudo chef-server-ctl org-list
pivotal
your_user
cubelab

The pivotal account is the built-in superuser and should not be used for daily work.

Step 3 - Installing and configuring Chef Workstation

On the workstation, download and inspect the same installer, then install Chef Workstation. It bundles the chef and knife commands, Cookstyle and Test Kitchen:

curl -fsSL https://omnitruck.chef.io/install.sh -o install.sh
less install.sh
sudo bash install.sh -c stable -P chef-workstation

Check the installation and accept the license when prompted:

chef -v
Chef Workstation version: 25.x.x
Chef Infra Client version: 18.x.x
Chef InSpec version: 5.x.x
Chef CLI version: 5.x.x
Test Kitchen version: 3.x.x
Cookstyle version: 7.x.x

Create the knife configuration directory and copy your user key from the server:

mkdir -p ~/.chef
scp [email protected]:your_user.pem ~/.chef/
chmod 600 ~/.chef/your_user.pem

Create the credentials file that tells knife who you are and which server and organization to use:

nano ~/.chef/credentials
[default]
client_name = "your_user"
client_key = "/home/your_user/.chef/your_user.pem"
chef_server_url = "https://chef.example.com/organizations/cubelab"

The server uses a self-signed certificate by default, so download it into knife's trusted certificates and check it:

knife ssl fetch
knife ssl check
Connecting to host chef.example.com:443
Successfully verified certificates from `chef.example.com'

Confirm that knife can authenticate against the organization. A new organization contains only its validator client:

knife client list
cubelab-validator

Step 4 - Writing a cookbook

Create a repository for your cookbooks and generate a new one:

mkdir -p ~/chef-repo/cookbooks
cd ~/chef-repo/cookbooks
chef generate cookbook nginx_site

The generator creates metadata.rb (name and version), recipes/default.rb, and test scaffolding for Test Kitchen and InSpec.

Define a default attribute for the page title. Attributes let you override values per node, role or environment without touching the recipe:

mkdir -p nginx_site/attributes
nano nginx_site/attributes/default.rb
default['nginx_site']['title'] = 'Managed by Chef'

Now write the recipe:

nano nginx_site/recipes/default.rb
package 'nginx'

service 'nginx' do
  action [:enable, :start]
end

template '/var/www/html/index.html' do
  source 'index.html.erb'
  owner 'root'
  group 'root'
  mode '0644'
  variables(title: node['nginx_site']['title'])
end

chef_client_systemd_timer 'chef-client' do
  interval '30min'
end

Each block is a resource with the desired state: the package is installed, the service is enabled and running, and the home page is rendered from a template. The last resource, built into Chef Infra Client, installs a systemd timer so the node converges every 30 minutes without manual runs.

Create the template:

mkdir -p nginx_site/templates
nano nginx_site/templates/index.html.erb
<!DOCTYPE html>
<html>
  <head><title><%= @title %></title></head>
  <body>
    <h1><%= @title %></h1>
    <p>Served by <%= node['fqdn'] %> running <%= node['platform'] %> <%= node['platform_version'] %>.</p>
  </body>
</html>

Lint the cookbook with Cookstyle, which catches syntax errors and deprecated patterns:

cookstyle nginx_site
1 file inspected, no offenses detected

The number of files depends on the generator output. Fix any offenses it reports, then upload the cookbook to the server:

knife cookbook upload nginx_site --cookbook-path ~/chef-repo/cookbooks
knife cookbook list
Uploading nginx_site     [0.1.0]
Uploaded 1 cookbook.
nginx_site   0.1.0

Step 5 - Bootstrapping the node

Bootstrapping connects to a node over SSH, installs Chef Infra Client, registers the node with the server and runs its first converge. From the workstation, run the following, replacing the user, key path and IP with your own:

knife bootstrap your_node_ip \
  --connection-user your_user \
  --ssh-identity-file ~/.ssh/id_ed25519 \
  --sudo \
  --node-name web1 \
  --run-list 'recipe[nginx_site]' \
  --chef-license accept

If the node does not resolve chef.example.com through DNS, add the /etc/hosts entry on the node before bootstrapping. At the end of the run you should see the resources being applied:

 [your_node_ip] Recipe: nginx_site::default
 [your_node_ip]   * apt_package[nginx] action install
 [your_node_ip]     - install version 1.24.0-2ubuntu7 of package nginx
 [your_node_ip]   * service[nginx] action enable (up to date)
 [your_node_ip]   * template[/var/www/html/index.html] action create
 [your_node_ip] Chef Infra Client finished, 6/12 resources updated in 38 seconds

The counts vary because chef_client_systemd_timer manages several resources internally.

Step 6 - Verifying the node

From the workstation, confirm that the server knows the node and its run list:

knife node show web1
Node Name:   web1
Environment: _default
FQDN:        web1
IP:          your_node_ip
Run List:    recipe[nginx_site]
Recipes:     nginx_site, nginx_site::default
Platform:    ubuntu 24.04

On the node, allow HTTP and check the page and the timer:

sudo ufw allow 'Nginx HTTP'
curl http://localhost
<!DOCTYPE html>
<html>
  <head><title>Managed by Chef</title></head>
  <body>
    <h1>Managed by Chef</h1>
    <p>Served by web1 running ubuntu 24.04.</p>
  </body>
</html>
systemctl list-timers 'chef-client*'
NEXT                        LEFT       LAST  PASSED  UNIT               ACTIVATES
Thu 2026-09-25 11:12:40 UTC 29min left -     -       chef-client.timer  chef-client.service

Step 7 - Changing the configuration

The normal workflow is: edit the cookbook, bump its version, upload it, and let nodes pick it up. Change the title in nginx_site/attributes/default.rb, then increase the version in nginx_site/metadata.rb:

version '0.1.1'

Upload the new version:

knife cookbook upload nginx_site --cookbook-path ~/chef-repo/cookbooks

The node applies it on its next timer run. To apply it immediately, run the client on the node:

sudo chef-client
Chef Infra Client finished, 1/12 resources updated in 6 seconds

Only the template changed, so only one resource was updated: Chef is idempotent and leaves everything already in the right state alone.

Troubleshooting

  • SSL Validation failure connecting to host on knife: run knife ssl fetch again, and make sure the URL in ~/.chef/credentials uses exactly the name in the server certificate (chef.example.com, not the IP).
  • 401 Unauthorized from knife: client_name does not match the user that owns the key, or the key file is wrong. Check both values in ~/.chef/credentials.
  • Bootstrap hangs at the sudo step: the SSH user needs passwordless sudo on the node. Configure it, or connect as a user that has it.
  • chef-server-ctl reconfigure fails or services crash: the server is short on memory. Check sudo chef-server-ctl tail and give the machine at least 8 GB of RAM.

Conclusion

You installed Chef Infra Server, configured Chef Workstation and knife, wrote and uploaded a cookbook with an attribute and a template, and bootstrapped a node that converges on a schedule. Next, you can keep ~/chef-repo in Git, test cookbooks locally with Test Kitchen before uploading, and use Policyfiles or environments to promote cookbook versions from staging to production.