Skip to content

ODROID-C4 Hardware Platform

๐Ÿ“Ÿ Hardware

odroidc4 ODROID-C4 is a new generation single board computer that is more energy efficient and faster performing than ODROID-C2 which was introduced over four years ago as the worldโ€™s first affordable ARM 64bit computer.

The main CPU of the ODROID-C4 is built with a quad-core Cortex-A55 cluster with a new generation Mali-G31 GPU. The A55 cores run at 2.0Ghz without thermal throttling using the stock heat sink allowing a robust and quiet computer. The CPU multi-core performance is around 40% faster, and the system DRAM performance is 50% faster than the ODROID-C2.

Type Description
Processor Amlogic S905X3 SoC - 4 x ARMยฎ Cortexยฎ-A55 2.0GHz
GPU 4 x ARMยฎ Mali-G31โ„ข 650MHz
RAM 4GB 32bit DDR4
Flash Storage Micro-SD UHS-I SDR104 or eMMC storage option
USB3.0 Host 4 Ports
Networking Gigabit Ethernet (10/100/1000 Mbit/s)
Expansion I/O 40pin GPIOs + 7pin I2S
Power 12V/2A - Stress: 3.64W - Idle: 0.18W
Dimensions 85 x 56mm
Weight 59g

Tip

This website is hosted on the ODROID-C4 configured with a debian operating system.
The following documentation has been made with MkDocs.

GPIO

odroidc4_gpio


๐Ÿ’ฟ Installing Debian

debian_banner

armbian_logo armbian is a free and open-source operating-system based on the debian linux system. An operating-system is the set of basic programs and utilities that make a computer run. Using armbian/debian on the odroid allows the access of thousands of cross-compiled packages for arm device.

A ssh server is installed by default in this release.


๐Ÿ› ๏ธ Installation

Download Trixie image

The first operation consists on downloading the armbian linux image and flashing it on eMMC.
Download the Trixie linux image for ODROID-C4 https://www.armbian.com/odroid-c4.

Flash Trixie image using Armbian Imager

Flash the Armbian_26.2.1_Odroidc4_trixie_current_6.18.8_minimal.img on the eMMC using Armbian Imager.
Click on Use Custom Image

armbian_imager


๐Ÿ” Login

debian_system

Login on armbian

Login using ssh with user: root, password: 1234 on ip: <device-ip>:

ssh root@<device-ip>`
  1. Set a new root password
  2. Create a new user (e.g. debian)
  3. Set the user password
  4. Enter the full name (e.g. Debian)
  5. Configure locale: skip
Add debian user account to sudo group if necessary

During the first connection on the odroid system a new user account will be created.
If this is not the case you will have to manually create a user account:

# add new user: debian
apt-get install sudo
sudo adduser debian
sudo usermod -aG sudo debian

โš™๏ธ System Configuration

Configure Environment

To setup debian for the ODROID-C4, follow those steps.

# update system
sudo apt-get clean
sudo rm -rf /var/lib/apt/lists/*
sudo apt-get update --allow-releaseinfo-change
sudo apt-get upgrade

# update linux kernel
sudo apt-get dist-upgrade
sudo apt-get autoremove --purge
sudo apt-get clean

# disable armbian logging services
sudo systemctl disable armbian-ramlog.service
sudo systemctl disable armbian-zram-config.service
sudo sed -i 's/ENABLED=true/ENABLED=false/g' /etc/default/armbian-ramlog
sudo rm /etc/cron.d/armbian-truncate-logs
sudo rm /etc/cron.daily/armbian-ram-logging

# force cpufreq governor to schedutil
sudo sed -i 's/ENABLE=.*/ENABLE=true/g' /etc/default/cpufrequtils
sudo sed -i 's/GOVERNOR=.*/GOVERNOR=schedutil/g' /etc/default/cpufrequtils

# set hostname manually
sudo hostnamectl set-hostname odroid

# configure sshd daemon
sudo sed -i 's/#*MaxAuthTries [0-9].*/MaxAuthTries 3/g' /etc/ssh/sshd_config    # limit failed authentication attempts per connection to 3
sudo sed -i 's/#LoginGraceTime 2m/LoginGraceTime 20s/g' /etc/ssh/sshd_config    # allow only 20 seconds for a client to authenticate before disconnecting
sudo sed -i 's/X11Forwarding yes/X11Forwarding no/g' /etc/ssh/sshd_config       # disable X11 forwarding to reduce attack surface
sudo systemctl reload ssh

# reboot
sync
sudo reboot now
Install telnet

To access the debian board in case sshd crashes, telnetd will be installed.

LAN-only access

Telnet transmits data in plaintext, including passwords. It is only acceptable as an emergency fallback on a trusted LAN. Ensure port 23/tcp is never forwarded on your internet router.

# install telnet daemon
sudo apt-get install telnetd

# TCP wrappers: allow SSH from anywhere and telnet from LAN only
# (sshd on modern Debian ignores hosts.* but add it as a safety net)
echo 'sshd: ALL' | sudo tee -a /etc/hosts.allow
echo 'telnetd: 192.168.1.0/255.255.255.0' | sudo tee -a /etc/hosts.allow

# deny everything not explicitly allowed above
echo 'ALL: ALL' | sudo tee -a /etc/hosts.deny

Tip

To access the odroid device via telnet: telnet <device-ip>.

Install SSH Keys

The following procedure install ssh security keys from the client (ex: Windows) to the remote computer in order to login without the need of password.

# on desktop - Ed25519 is the recommended algorithm
ssh-keygen -t ed25519

Define host/port/user parameters

REMOTE_HOST="host"  # ex: dyndns.domain.fr
REMOTE_PORT="port"  # ex: 22
REMOTE_USER="user"  # ex: debian
$env:REMOTE_HOST="host"  # ex: dyndns.domain.fr
$env:REMOTE_PORT="port"  # ex: 22
$env:REMOTE_USER="user"  # ex: debian

Append local public key to remote computer

# verify both vars are set; otherwise print an error and exit
[ -n "$REMOTE_HOST" ] && [ -n "$REMOTE_PORT" ] && [ -n "$REMOTE_USER" ] || { echo "Error: set REMOTE_HOST, REMOTE_PORT, REMOTE_USER"; return 1 2>/dev/null || exit 1; }

# append local public key to remote authorized_keys (preserves existing entries)
ssh -p "$REMOTE_PORT" "$REMOTE_USER@$REMOTE_HOST" "mkdir -p ~/.ssh && chmod 700 ~/.ssh"
cat "$HOME/.ssh/id_ed25519.pub" | ssh -p "$REMOTE_PORT" "$REMOTE_USER@$REMOTE_HOST" "cat >> ~/.ssh/authorized_keys"
ssh -p "$REMOTE_PORT" "$REMOTE_USER@$REMOTE_HOST" "chmod 600 ~/.ssh/authorized_keys"
# verify both vars are set; otherwise print an error and exit
if (-not $env:REMOTE_HOST -or -not $env:REMOTE_PORT -or -not $env:REMOTE_USER) { Write-Error "Error: set REMOTE_HOST, REMOTE_PORT, REMOTE_USER"; return }

# append local public key to remote authorized_keys (preserves existing entries)
ssh -p $env:REMOTE_PORT "$($env:REMOTE_USER)@$($env:REMOTE_HOST)" "mkdir -p ~/.ssh && chmod 700 ~/.ssh"
Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" | ssh -p $env:REMOTE_PORT "$($env:REMOTE_USER)@$($env:REMOTE_HOST)" "cat >> ~/.ssh/authorized_keys"
ssh -p $env:REMOTE_PORT "$($env:REMOTE_USER)@$($env:REMOTE_HOST)" "chmod 600 ~/.ssh/authorized_keys"

Info

It's now possible to login without entering password using ssh -p <port> <user>@<host>

Configure Firewall (ufw)

The goal of a firewall on Linux is to control and manage network traffic based on predetermined security rules. It acts as a barrier between a trusted internal network and untrusted external networks, such as the internet.

Install ufw Firewall

ufw is a straightforward command-line tool that manages iptables configurations, the core firewall infrastructure of Linux. Docker also interacts with these iptables rules, often overriding ufw settings and enforcing its own rules.

# install ufw firewall
sudo apt-get install ufw

# switch iptables backend to nftables for Docker/ufw compatibility
sudo update-alternatives --set iptables /usr/sbin/iptables-nft
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-nft
sudo iptables -L -n
Reset ufw and clear firewall state

The following commands fully reset the firewall by removing any stale or partially applied UFW and iptables rules.
This is useful after system upgrades or when UFW is in a corrupted or inconsistent state.

# stop services that may interfere with firewall rules
sudo systemctl stop docker            # stop Docker to prevent rule conflicts

# disable and reset UFW
sudo ufw disable                      # disable firewall to avoid conflicts during cleanup
sudo ufw reset                        # remove all UFW rules and configuration

# flush all existing iptables rules (IPv4 and IPv6)
sudo iptables -F                      # clear all IPv4 rules
sudo iptables -X                      # delete all custom IPv4 chains
sudo ip6tables -F                     # clear all IPv6 rules
sudo ip6tables -X                     # delete all custom IPv6 chains

Configuring ufw Firewall

The following commands will configure the Linux firewall.

# disable IPv6 support in UFW (workaround for ip6tables/logging rule errors)
sudo sed -i 's/^IPV6=yes/IPV6=no/' /etc/default/ufw

# clean ufw rules
sudo ufw disable                    # disable firewall before blocking connections => otherwise ssh will stop
sudo ufw reset                      # reset all firewall settings
sudo ufw default deny incoming      # deny all incoming connections by default
sudo ufw default allow outgoing     # allow all outgoing connections by default

# exposes ports of containers and hosts for specific port
sudo ufw allow 22/tcp               # allow ssh
sudo ufw allow 80/tcp               # allow http
sudo ufw allow 443/tcp              # allow https
sudo ufw allow 445/tcp              # allow samba
sudo ufw allow 1194/udp             # allow openvpn
sudo ufw allow from 192.168.1.0/24 to any port 23 proto tcp   

# activate the new ufw rules
sudo ufw enable                     # enable the firewall with new rules
sudo ufw status verbose             # show firewall status

# start Docker service
sudo systemctl start docker
Docker and ufw

If you are using the Uncomplicated Firewall (ufw) alongside Docker, you may encounter situations where your Dockerized web applications remain accessible from the internet despite ufw settings. This issue arises because both ufw and Docker manipulate the same underlying iptables configurations, potentially leading to conflicting rules that expose Docker containers regardless of ufw's active state.

A recommended approach involves prioritizing the ufw rules over those of Docker. This can be accomplished using the following command lines:

# force ufw rules before docker's
sudo tee -a /etc/ufw/after.rules > /dev/null <<EOF
# Put Docker behind UFW
*filter
:DOCKER-USER - [0:0]
:ufw-user-input - [0:0]

-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-USER -m conntrack --ctstate INVALID -j DROP
-A DOCKER-USER -i eth0 -j ufw-user-input
-A DOCKER-USER -i eth0 -j DROP
COMMIT
EOF

# restart and reload ufw rules
sudo ufw reload
Protect Against Brute-Force Attacks (fail2ban)

Fail2ban is a lightweight intrusion prevention tool.
It monitors service logs (like SSH) and automatically bans IP addresses that show malicious signs, such as too many failed login attempts in a short time.
This prevents brute-force attacks from overwhelming your server with endless authentication attempts.

Install fail2ban

# install fail2ban
sudo apt-get install -y fail2ban

Configure fail2ban for SSH and NGINX

NGINX's own limit_req zones only answer 429 and then forget the client โ€” they slow an attacker down but never stop one. The two NGINX jails below turn that throttling into an actual ban, and cover every virtual host at once.

# configure global fail2ban settings (applies to all jails)
sudo tee /etc/fail2ban/fail2ban.local > /dev/null <<EOF
[DEFAULT]
allowipv6 = auto
EOF

# configure the jails: SSH, plus the two NGINX ones
sudo tee /etc/fail2ban/jail.local > /dev/null <<EOF
[DEFAULT]
ignoreip  = 127.0.0.1/8 ::1 192.168.0.0/16 10.0.0.0/8

[sshd]
enabled   = true
port      = 22
logpath   = /var/log/auth.log
maxretry  = 3
findtime  = 10m
bantime   = 24h

# Ban whoever keeps tripping a limit_req zone. Uses the filter that ships
# with fail2ban and reads NGINX's own error log, so it needs no knowledge
# of any site: every vhost with a rate limiter is covered automatically.
[nginx-limit-req]
enabled   = true
port      = http,https
logpath   = /var/log/nginx/error.log
maxretry  = 10
findtime  = 10m
bantime   = 24h

# Ban repeated failed logins across the whole fleet. The filter below
# matches the login routes of both the own projects and the cloned apps.
[nginx-login-fail]
enabled   = true
port      = http,https
logpath   = /var/log/nginx/access.log
maxretry  = 15
findtime  = 10m
bantime   = 24h
EOF

Install the login-failure filter

The regex matches the combined access-log format Debian's NGINX writes by default. Keep the path alternation in step with the rate-limited location blocks of each site.

# filter for the [nginx-login-fail] jail declared above
sudo tee /etc/fail2ban/filter.d/nginx-login-fail.conf > /dev/null <<'EOF'
[Definition]
failregex = ^<HOST> \S+ \S+ \[[^\]]+\] "(GET|POST) /(api/auth/(login|activate|confirm-email|bootstrap)|identity/connect/token|api/access-tokens|admin|login)\S* [^"]+" (401|403|429)
ignoreregex =
EOF

# dry-run the filter against the real log before enabling anything
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-login-fail.conf

# enable and start the service
sudo systemctl enable --now fail2ban

# check that the service is running
sudo systemctl status fail2ban

fail2ban-regex reports how many lines the pattern matched. Zero matches on a log that does contain failed logins means the regex is wrong โ€” a jail that matches nothing looks exactly like one that works.

Verify and monitor bans

# show current status of a jail (number of failures, active bans, etc.)
sudo fail2ban-client status sshd
sudo fail2ban-client status nginx-limit-req
sudo fail2ban-client status nginx-login-fail

# monitor authentication log for incoming attempts
sudo tail -n 25 /var/log/auth.log

# monitor fail2ban's own actions (bans, unbans, warnings)
sudo tail -n 25 /var/log/fail2ban.log

# lift a ban that caught you out
sudo fail2ban-client set nginx-login-fail unbanip 203.0.113.10

๐Ÿ’พ Configure SSD

Detect SSD device
# detect device name
lsblk
*NAME         MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda            8:0    0 465.8G  0 disk 
โ””โ”€sda1         8:1    0 465.8G  0 part 
mmcblk1      179:0    0 116.5G  0 disk 
โ””โ”€mmcblk1p1  179:1    0 115.3G  0 part /

# store device name
DEVICE=/dev/sda

Tip

The SSD device name here is: /dev/sda.

Format SSD in ext4
# install parted
sudo apt-get install parted

# check that device partition has been selected
[ -n "${DEVICE:-}" ] && [ -b "${DEVICE}" ] || { echo "Invalid or missing block device: ${DEVICE:-<empty>}"; exit 1; }

# print what is about to be formatted
lsblk -o NAME,SIZE,MODEL,SERIAL,TRAN "${DEVICE}"

# ask user to continue
read -r -p "โš ๏ธ  This will ERASE ALL DATA on ${DEVICE}. Type YES to continue: " CONFIRM
[ "${CONFIRM}" = "YES" ] || { echo "Aborted."; exit 1; }

# create a GPT partition table 
sudo parted "${DEVICE}" --script -- mklabel gpt

# create ext4 partition that takes the whole space
sudo parted "${DEVICE}" --script -- mkpart primary ext4 0% 100%

# ensure kernel sees the new partition table
sudo partprobe "${DEVICE}" || true
sudo udevadm settle || true

# format partition to ext4
PART="${DEVICE}1"
[ -b "${PART}" ] || { echo "Partition not found: ${PART}"; exit 1; }
sudo mkfs.ext4 -F "${PART}"
sync

# verify the partition table
sudo parted "${DEVICE}" --script print

Tip

Set DEVICE with your SSD device name (ex: /dev/sda).

Mount SSD automatically

The following steps describe how to mount automatically the SSD using its UUID.

# install setfacl/getfacl packages
sudo apt-get install acl

# define mount point for the SSD
SSD_DIR=/media/ssd

# ensure SSD_DIR variable is defined and not empty
[ -n "${SSD_DIR:-}" ] || { echo "SSD_DIR is not defined"; exit 1; }

# display available partitions to help user choose
echo "Available partitions:"
lsblk -fp -o NAME,FSTYPE,SIZE,LABEL,UUID,MOUNTPOINT

# ask user which partition should be mounted automatically
read -r -p "Enter the partition to mount (example: /dev/sda1): " PART
[ -b "${PART}" ] || { echo "Not a valid block device: ${PART}"; exit 1; }

# ensure the filesystem is ext4
FSTYPE="$(sudo blkid -p -s TYPE -o value "${PART}" 2>/dev/null || true)"
[ -n "${FSTYPE}" ] || { echo "No filesystem detected on ${PART}"; exit 1; }
[ "${FSTYPE}" = "ext4" ] || { echo "Expected ext4, got ${FSTYPE} on ${PART}"; exit 1; }

# extract UUID of the selected partition
SSD_UUID="$(sudo blkid -s UUID -o value "${PART}")"
[ -n "${SSD_UUID}" ] || { echo "Could not read UUID for ${PART}"; exit 1; }

# create mount directory if it does not exist
sudo mkdir -p "${SSD_DIR}"

# refuse modification if UUID or mountpoint already exists in /etc/fstab
if grep -Eq "^[[:space:]]*UUID=${SSD_UUID}[[:space:]]" /etc/fstab || grep -Eq "[[:space:]]${SSD_DIR}[[:space:]]" /etc/fstab; then
  echo "An fstab entry for UUID=${SSD_UUID} or mountpoint ${SSD_DIR} already exists. Not modifying /etc/fstab."
  exit 1
fi

# append new mount entry to /etc/fstab
echo "UUID=${SSD_UUID} ${SSD_DIR} ext4 defaults,noatime,nodiratime,commit=60,errors=remount-ro 0 2" | sudo tee -a /etc/fstab > /dev/null

# mount the filesystem immediately
sudo systemctl daemon-reload
sudo mount "${SSD_DIR}"

Replace the following fields

Set SSD_DIR with your SSD mount directory (ex: /media/ssd)

Configure TRIM for SSD/eMMC Devices

eMMC and SSD storage have a limited number of write cycles. Without proper maintenance, excessive write operations can shorten their lifespan, potentially leading to data corruption or device failure. Enabling TRIM helps the operating system inform the storage device which blocks of data are no longer in use, allowing it to manage wear more efficiently and extend the deviceโ€™s durability.

# enable systemd TRIM
sudo systemctl enable fstrim.timer
sudo systemctl start fstrim.timer

# see fstrim status
systemctl status fstrim.timer

# check last run time
systemctl list-timers | grep fstrim

# run manually
sudo fstrim --all -v
Optimize Filesystem and Memory Settings for SSD/eMMC Devices

These system tweaks are recommended to reduce unnecessary write operations on eMMC or SSD devices, extending their lifespan and improving performance:

  • Suppress Access Time Updates: Adds nodiratime to reduce metadata writes for directory access.
  • Extend Commit Interval: Sets commit=60 to flush metadata changes every 60 seconds instead of more frequently.
# add nodiratime where noatime is present and nodiratime is missing (ext4 only)
sudo sed -i -E '/[[:space:]]ext4[[:space:]]/ {
  /noatime/ {
    /nodiratime/! s/noatime/noatime,nodiratime/
  }
}' /etc/fstab

# set commit=60 if commit= is present; otherwise add it (ext4 only)
sudo sed -i -E '/[[:space:]]ext4[[:space:]]/ {
  s/(^|,)commit=[0-9]+/\1commit=60/g
  /commit=60/! s/[[:space:]](defaults[^[:space:]]*)/\1,commit=60/
}' /etc/fstab

# mount the filesystem immediately
sudo mount -a

๐Ÿ’พ Configure SWAP

Configure SWAP (zram + swapfile hybrid)

This configuration combines fast compressed RAM swap (zram) with a large disk-based swapfile fallback. It is tuned for an ODROID-C4 with 4GB RAM, including workloads that can temporarily consume large amounts of memory such as parallel Rust compilation, linking, and compression.

  • zram (~1GB logical size, high priority) โ†’ Used first. Swapped pages are compressed and kept in RAM, providing much lower latency than disk swap and avoiding storage writes.
  • 25% zram memory limit โ†’ Prevents zram from consuming an excessive amount of physical RAM.
  • Multi-core zram compression โ†’ Modern kernels use the available CPUs automatically. ZRAM_MAX_DEVICES=4 is retained for compatibility with Armbian/kernel versions exposing the older compression-stream limit.
  • LZO/LZO-RLE compression โ†’ Favors fast compression/decompression with low CPU overhead, making it well suited to ARM systems.
  • Swapfile (8GB, low priority) โ†’ Used only after higher-priority zram becomes insufficient. The large fallback is useful for exceptional memory-intensive workloads.
  • Swappiness 80 โ†’ Allows the kernel to use swap reasonably early instead of waiting until memory pressure becomes critical.
  • Overcommit enabled โ†’ Allows flexible virtual memory allocation, which can be useful for compilation, linking, compression, and applications that temporarily reserve large amounts of address space.
  • Swap readahead disabled โ†’ vm.page-cluster=0 avoids unnecessary swap readahead and is well suited to low-latency zram-backed swap.
zram does not reserve 1GB of RAM

A 1GB zram device is a logical compressed block device. It does not reserve 1GB of physical RAM when the system starts. Physical RAM is consumed dynamically only when pages are actually swapped into zram. For example, if 1GB of swapped data compresses to roughly 500MB, zram consumes approximately 500MB of physical RAM plus a small amount of metadata overhead. The actual compression ratio depends entirely on the workload.

Swap priority determines which swap device is used first

vm.swappiness does not determine whether Linux prefers zram or the disk swapfile. Swap devices are selected according to their swap priority.

This configuration uses:

zram       priority 5
swapfile   priority 0

Linux therefore uses zram first, then falls back to the disk swapfile when necessary.

Why use an 8GB swapfile?

An 8GB swapfile is intentionally used because some workloads can temporarily require considerably more memory than the ODROID-C4's 4GB of physical RAM.

This is particularly useful for:

  • Parallel Rust compilation
  • Large linking operations
  • Compression/decompression workloads
  • Multiple concurrent compiler processes
  • Memory-intensive build or maintenance tasks

The swapfile acts primarily as an emergency capacity extension. Because it has a much lower priority than zram, normal memory pressure is handled by compressed RAM first. If the system begins actively using several gigabytes of disk swap, it may remain operational instead of immediately running out of memory, but performance can become considerably slower. Prolonged heavy swapping also increases write activity on eMMC, SD cards, and SSDs.

Memory overcommit

vm.overcommit_memory=1 allows Linux to approve virtual memory allocations without performing the normal heuristic commitment checks. This is useful for workloads that reserve large amounts of virtual memory but do not necessarily use all of it. Overcommit does not create additional memory. If applications eventually consume more memory than the available RAM, zram, and disk swap can provide, the kernel may still invoke the OOM killer.

# ------------------------------------------------------------------
# Stop SWAP cleanly
# ------------------------------------------------------------------
sudo swapoff -a
sudo systemctl stop armbian-zram-config.service

# ------------------------------------------------------------------
# Reset any unused zram devices left by a previous configuration.
#
# Mounted zram devices are deliberately left untouched.
# ------------------------------------------------------------------
for ZRAM_DEV in /dev/zram*; do
  [ -e "$ZRAM_DEV" ] || continue

  if ! sudo swapon --show --noheadings --raw --output NAME \
        | grep -Fxq "$ZRAM_DEV" \
    && ! findmnt -rn -S "$ZRAM_DEV" > /dev/null; then
    sudo /usr/sbin/zramctl --reset "$ZRAM_DEV" 2>/dev/null || true
  fi
done

# ------------------------------------------------------------------
# Configure Armbian zram
# ------------------------------------------------------------------
CONFIG="/etc/default/armbian-zram-config"
update_var() {
  VAR="$1"
  VALUE="$2"
  if grep -qE "^[# ]*${VAR}=" "$CONFIG"; then
    sudo sed -i -E "s|^[# ]*${VAR}=.*|${VAR}=${VALUE}|" "$CONFIG"
  else
    echo "${VAR}=${VALUE}" | sudo tee -a "$CONFIG" > /dev/null
  fi
}

update_var ENABLED true
update_var SWAP true
update_var ZRAM_PERCENTAGE 25       # ~1GB logical zram device on a system with 4GB RAM
update_var MEM_LIMIT_PERCENTAGE 25  # Limit zram to approximately 25% of physical RAM.
update_var SWAP_ALGORITHM lzo       # Fast compression algorithm well suited to ARM systems.
update_var ZRAM_MAX_DEVICES 4       # Compatibility with kernels exposing max_comp_streams.
update_var SWAP_PRIORITY 5          # Keep zram above the disk swapfile.

sudo systemctl enable armbian-zram-config.service
sudo systemctl restart armbian-zram-config.service

# ------------------------------------------------------------------
# Create an 8GB disk swapfile as LOW-priority fallback
# ------------------------------------------------------------------
sudo fallocate -l 8G /var/swapfile
sudo chmod 600 /var/swapfile
sudo mkswap /var/swapfile

# ------------------------------------------------------------------
# Persist the swapfile across reboots
#
# Priority 0 keeps disk swap below zram (priority 5).
#
# Swap priority determines which swap device Linux uses first.
# ------------------------------------------------------------------
if grep -qE '^/var/swapfile[[:space:]]' /etc/fstab; then
  sudo sed -i -E \
    's|^/var/swapfile[[:space:]].*|/var/swapfile none swap sw,pri=0 0 0|' \
    /etc/fstab
else
  echo '/var/swapfile none swap sw,pri=0 0 0' \
    | sudo tee -a /etc/fstab > /dev/null
fi

# ------------------------------------------------------------------
# Enable the disk swapfile now
# ------------------------------------------------------------------
sudo swapon -p 0 /var/swapfile

# ------------------------------------------------------------------
# Tune memory behavior
# ------------------------------------------------------------------
# Remove possible conflicting swappiness definitions from common
# configuration files before installing our dedicated configuration.
sudo sed -i -E \
  's/^[[:space:]]*vm\.swappiness[[:space:]]*=/# &/' \
  /etc/sysctl.conf 2>/dev/null || true
sudo sed -i -E \
  's/^[[:space:]]*vm\.swappiness[[:space:]]*=/# &/' \
  /etc/sysctl.d/99-sysctl.conf 2>/dev/null || true

# ------------------------------------------------------------------
# Swappiness
# ------------------------------------------------------------------
echo "vm.swappiness=80" \
  | sudo tee /etc/sysctl.d/99-swappiness.conf > /dev/null

# ------------------------------------------------------------------
# Memory overcommit
# ------------------------------------------------------------------
echo "vm.overcommit_memory=1" \
  | sudo tee /etc/sysctl.d/99-overcommit.conf > /dev/null

# ------------------------------------------------------------------
# Swap readahead
# ------------------------------------------------------------------
echo "vm.page-cluster=0" \
  | sudo tee /etc/sysctl.d/99-page-cluster.conf > /dev/null

# ------------------------------------------------------------------
# Reload all sysctl configuration
# ------------------------------------------------------------------
sudo sysctl --system

# ------------------------------------------------------------------
# Verify final configuration
# ------------------------------------------------------------------
echo
echo "=== Swap devices ==="
sudo swapon --show

echo
echo "=== zram status ==="
sudo /usr/sbin/zramctl

echo
echo "=== Memory ==="
free -h

echo
echo "=== Kernel memory settings ==="

printf "swappiness:          "
cat /proc/sys/vm/swappiness

printf "overcommit_memory:   "
cat /proc/sys/vm/overcommit_memory

printf "page-cluster:        "
cat /proc/sys/vm/page-cluster

# Locate the zram device actually being used as swap instead of
# assuming that it is always /dev/zram0.
ZRAM_SWAP=$(
  sudo swapon --show --noheadings --raw --output NAME |
  awk '$1 ~ "^/dev/zram[0-9]+$" { print $1; exit }'
)

if [ -n "$ZRAM_SWAP" ]; then
  ZRAM_NAME="${ZRAM_SWAP##*/}"

  echo
  echo "=== Active zram swap: $ZRAM_SWAP ==="

  if [ -e "/sys/block/$ZRAM_NAME/comp_algorithm" ]; then
    printf "algorithm:           "
    cat "/sys/block/$ZRAM_NAME/comp_algorithm"
  fi

  if [ -e "/sys/block/$ZRAM_NAME/max_comp_streams" ]; then
    printf "compression streams: "
    cat "/sys/block/$ZRAM_NAME/max_comp_streams"
  else
    echo "compression streams: managed automatically by kernel"
  fi

  if [ -r "/sys/block/$ZRAM_NAME/mm_stat" ]; then
    printf "memory limit:        "
    awk '{print $4}' "/sys/block/$ZRAM_NAME/mm_stat" | numfmt --to=iec
  fi
else
  echo
  echo "WARNING: no active zram swap device found"
fi

echo
echo "=== Expected swap priorities ==="
echo "zram:       5"
echo "swapfile:   0"