| 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
/rootmarkers and a post-reboot verification script - Run
systemctl enablefor units that should start on first boot - Copy
/tmpinstaller 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:
%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:
cat /root/ks-lab-marker.txtkickstart-post-okInspect the post log for disk and hostname lines:
sudo head -15 /var/log/kickstart_post.log=== %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_memberThe 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:
%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
%endOn the installed system, confirm the copied pre log:
ls -la /var/log/kickstart_pre.log /var/log/anaconda.log-rw-r--r--. 1 root root ... /var/log/anaconda.log
-rw-r--r--. 1 root root ... /var/log/kickstart_pre.logThis 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:
%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())
%endAfter install, read the interpreter logs:
cat /var/log/kickstart_bash_post.log
cat /var/log/kickstart_python_post.logbash interpreter %post on vm2.lab.example at 2026-08-09T20:41:29+05:30
python interpreter %post at 2026-08-09T20:41:29.492260Example 4: %post without --erroronfail
The harness adds a section without --erroronfail to test marker copy from a %pre block that omitted --log:
%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
%endAfter install, when %post --nochroot copied the markers:
cat /var/log/kickstart_post_no_erroronfail.logpost section without --erroronfail 2026-08-09T20:41:29+05:30
pre-no-log-marker copied
pre-without-log-marker copiedExample 5: Install verification script in %post
Embed a script on the installed system for post-reboot checks:
%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
%endAfter reboot, run the embedded checker:
sudo /root/ks-lab-verify.sh=== 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:
systemctl get-defaultgraphical.targetReferences
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.

