Kickstart %post Script Examples on RHEL

Deepak Prasad
Tested on RHEL 10.2 (Coughlan) — kickstart harness on vm2.lab.example via PXE from vm1.lab.example
Package pykickstart 3.52.12-1.el10
Applies to RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora
Privilege Installer boot access on the target host
Scope %post timing, chroot vs --nochroot, --log and --interpreter, copying %pre logs, harness verification script, and systemctl enable in chroot. Does not cover PXE server build or full kickstart grammar.
Related guides Automate RHEL installation with Kickstart
Kickstart %pre script examples
Configure Kickstart PXE boot server
systemctl command in Linux
df and du command in Linux

Kickstart %post runs after package installation completes but before Anaconda reboots into the new system. By default the script executes inside a chroot of the installed root at /mnt/sysimage. That is where you drop config files, enable services for first boot, and leave markers that prove automation finished.

Typical %post jobs:

  • Write /root markers and a post-reboot verification script
  • Run systemctl enable for units that should start on first boot
  • Copy /tmp installer logs into /var/log/ with %post --nochroot

This article covers %post examples from the RHEL 10 lab harness on vm2. For %pre timing and disk wipe scripts, see Kickstart %pre script examples. Kickstart file structure and ksvalidator live in Automate RHEL installation with Kickstart.


What is a Kickstart %post script?

Property Detail
Timing After packages install, before first reboot
Default environment Chrooted to installed system root
Syntax Section starts with %post and ends with %end
Multiple sections You can add several %post blocks in one file

Use %post for extra packages from custom repos, config files, markers, and systemctl enable. Use %post --nochroot to act in the installer environment—for example copying /tmp logs into /mnt/sysimage/var/log/.


systemctl in %post

In the default chrooted %post, systemctl start and systemctl status usually fail because systemd is not running in the chroot. systemctl enable and systemctl disable work. They only create or remove unit symlinks under /etc/systemd/system.

Prefer kickstart services --enabled=chronyd,sshd for standard services. In %post, use systemctl enable when you need conditional logic. For installer-side actions, use %post --nochroot or see systemctl command in Linux.


Example 1: Chrooted %post with --log and --erroronfail

The harness logs state, writes markers, and enables chronyd:

text
%post --erroronfail --log=/var/log/kickstart_post.log
#!/bin/bash
echo "=== %post chroot $(date -Is) ==="
df -Th
hostname -f
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
ip -br addr
nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null || true
echo "kickstart-post-ok" > /root/ks-lab-marker.txt
echo "graphical-server-environment" > /root/ks-lab-environment.txt
echo "bond+bond0.56 vlan" > /root/ks-lab-network.txt
systemctl enable chronyd
systemctl enable gdm
systemctl set-default graphical.target
cat > /etc/ssh/sshd_config.d/01-lab-ssh.conf << 'SSHEOF'
# Lab harness: rootpw plus sshkey for password and key login (not for production)
PermitRootLogin yes
PasswordAuthentication yes
PubkeyAuthentication yes
SSHEOF
%end

--erroronfail stops the install if the script exits non-zero. Keep it on %post blocks while you are still testing new logic.

@^graphical-server-environment in %packages installs GDM and GNOME, but Anaconda does not always set graphical.target as the default boot mode. The harness enables GDM and runs systemctl set-default graphical.target here so the first reboot lands on a GUI login, not a text console.

After reboot, confirm the marker file exists:

bash
cat /root/ks-lab-marker.txt
output
kickstart-post-ok

Inspect the post log for disk and hostname lines:

bash
sudo head -15 /var/log/kickstart_post.log
output
=== %post chroot 2026-08-09T20:41:28+05:30 ===
Filesystem                  Type      Size  Used Avail Use% Mounted on
/dev/mapper/rhel_vm200-root xfs        16G  5.8G   11G  37% /
/dev/sda2                   xfs       2.0G  509M  1.5G  26% /boot
vm2.lab.example
NAME                 SIZE TYPE FSTYPE      MOUNTPOINT
  |-rhel_vm200-root   16G lvm  xfs         /
sdb                   10G disk
`-sdb1                10G part LVM2_member

The log should show the new LVM root mounted at /, the FQDN from kickstart, and sdb left untouched by ignoredisk. Anaconda names the volume group from the hostname (rhel_vm200 on vm2.lab.example), not from a fixed rhel_vm2 label in the kickstart file.


Example 2: %post --nochroot to copy installer logs

Logs from %pre live in /tmp in the installer environment. Copy them before reboot:

text
%post --nochroot --log=/mnt/sysimage/var/log/kickstart_post_nochroot.log
#!/bin/bash
echo "=== %post --nochroot $(date -Is) ==="
INSTALLER_LOGS="/tmp/anaconda.log /tmp/storage.log /tmp/packaging.log /tmp/program.log /tmp/syslog /tmp/ks-lab-run-tag"
for f in $INSTALLER_LOGS /tmp/kickstart_pre.log /tmp/kickstart_pre_install.log /tmp/pre-no-log-marker; do
  [[ -f "$f" ]] && cp -av "$f" /mnt/sysimage/var/log/
done
[[ -d /tmp/anaconda ]] && cp -av /tmp/anaconda /mnt/sysimage/var/log/anaconda-installer-tmp 2>/dev/null || true
df -Th
ip -br link
%end

On the installed system, confirm the copied pre log:

bash
ls -la /var/log/kickstart_pre.log /var/log/anaconda.log
output
-rw-r--r--. 1 root root  ... /var/log/anaconda.log
-rw-r--r--. 1 root root  ... /var/log/kickstart_pre.log

This pairs with Kickstart %pre script examples sections that write /tmp/kickstart_pre.log.


Example 3: --interpreter for Python and Bash

Run separate interpreters in distinct %post sections:

text
%post --interpreter=/usr/bin/bash --log=/var/log/kickstart_bash_post.log
echo "bash interpreter %post on $(hostname -f) at $(date -Is)"
%end

%post --interpreter=/usr/libexec/platform-python --log=/var/log/kickstart_python_post.log
print("python interpreter %post at", __import__("datetime").datetime.now().isoformat())
%end

After install, read the interpreter logs:

bash
cat /var/log/kickstart_bash_post.log
cat /var/log/kickstart_python_post.log
output
bash interpreter %post on vm2.lab.example at 2026-08-09T20:41:29+05:30
python interpreter %post at 2026-08-09T20:41:29.492260

Example 4: %post without --erroronfail

The harness adds a section without --erroronfail to test marker copy from a %pre block that omitted --log:

text
%post --log=/var/log/kickstart_post_no_erroronfail.log
#!/bin/bash
echo "post section without --erroronfail $(date -Is)"
if [[ -f /var/log/pre-no-log-marker ]]; then echo "pre-no-log-marker copied"; fi
if [[ -f /var/log/pre-without-log-marker ]]; then echo "pre-without-log-marker copied"; fi
%end

After install, when %post --nochroot copied the markers:

bash
cat /var/log/kickstart_post_no_erroronfail.log
output
post section without --erroronfail 2026-08-09T20:41:29+05:30
pre-no-log-marker copied
pre-without-log-marker copied

Example 5: Install verification script in %post

Embed a script on the installed system for post-reboot checks:

text
%post --erroronfail --log=/var/log/kickstart_post.log
#!/bin/bash
cat > /root/ks-lab-verify.sh << 'VERIFY'
#!/bin/bash
echo "=== ks-lab verification $(date -Is) ==="
ls -la /var/log/kickstart* 2>/dev/null
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
ip -br addr
nmcli con show 2>/dev/null | grep -E "bond|vlan|NAME" || true
cat /root/ks-lab-marker.txt /root/ks-lab-environment.txt 2>/dev/null
cat /etc/redhat-release
VERIFY
chmod +x /root/ks-lab-verify.sh
%end

After reboot, run the embedded checker:

bash
sudo /root/ks-lab-verify.sh
output
=== ks-lab verification 2026-08-09T21:16:35+05:30 ===
  |-rhel_vm200-root   16G lvm  xfs         /
sdb                   10G disk
`-sdb1                10G part LVM2_member
bond0.56@bond0   UP             192.168.56.220/24
kickstart-post-ok
graphical-server-environment
bond+bond0.56 vlan
Red Hat Enterprise Linux release 10.2 (Coughlan)

Confirm the default target is graphical after the harness %post block:

bash
systemctl get-default
output
graphical.target

References


Summary

Use chrooted %post for configuration on the installed tree: markers under /root, systemctl enable, and files under /etc. Use %post --nochroot when you need the installer environment, especially to copy /tmp logs into /mnt/sysimage/var/log/ before reboot.

Combine --log= with --erroronfail while you are still testing. Split chrooted work, log copy, and interpreter demos into separate %post blocks so one failure does not hide which section broke. Once the harness is stable, relax --erroronfail on optional sections that only check marker files.


Frequently Asked Questions

1. Does %post run before or after the first reboot?

%post runs after package installation completes but before Anaconda reboots into the installed system. Changes in %post are on disk before the first boot of the new OS.

2. Can I run systemctl start in a default %post section?

Most systemctl start or status commands fail in the default chrooted %post because systemd is not running in the installed environment. systemctl enable and disable work because they only adjust unit file symlinks. Use %post --nochroot for installer-side actions or kickstart services --enabled for boot-time services.

3. What is the difference between default %post and %post --nochroot?

Default %post runs inside a chroot of /mnt/sysimage so paths like /etc and /root refer to the installed system. %post --nochroot runs in the installer environment so you can copy files from /tmp into /mnt/sysimage before reboot.

4. Can I run subscription-manager in %post?

Yes on RHEL when network and credentials are available, but the rhsm kickstart command is simpler for activation-key installs. Use %post only when you need custom registration logic that rhsm cannot express.

5. Where do %post log files land after reboot?

Paths in --log= are relative to the chroot root for default %post, so /var/log/kickstart_post.log ends up on the installed system. For %post --nochroot use /mnt/sysimage/var/log/ in the --log= path.

6. What happens when %post --erroronfail exits with an error?

Anaconda stops the install and reports the script failure. Remove --erroronfail on optional sections once the harness is stable so a non-critical marker check does not block the whole run.

7. Can one kickstart file contain multiple %post sections?

Yes. Anaconda runs each %post block in file order. Split chrooted config, --nochroot log copy, and interpreter demos into separate sections so one failure is easier to isolate.

8. Why do I still boot to a text login after installing graphical-server-environment?

The GUI packages and GDM can be installed while the default target stays multi-user.target. Run systemctl set-default graphical.target and systemctl enable gdm in %post. inst.text on the PXE kernel line does not change the post-install boot mode.
Omer Cakmak

Linux Administrator

Highly skilled at managing Debian, Ubuntu, CentOS, Oracle Linux, and Red Hat servers. Proficient in bash scripting, Ansible, and AWX central server management, he handles server operations on OpenStack, KVM, Proxmox, and VMware.

  • Debian
  • Ubuntu
  • Linux
  • Red Hat Enterprise Linux
  • Shell Script
  • System Administration