- Operating System: Ubuntu Linux
- Hypervisor: QEMU/KVM managed via virt-manager
- VM Specifications: 6 GB RAM, 4 CPU cores
Before beginning the deployment, I evaluated several Linux distributions including Arch Linux, Ubuntu, Debian, RHEL, and Rocky/AlmaLinux.
Final Recommendation: For this specific assessment environment (and general internal platform/developer use), Ubuntu is the optimal choice. It provides a massive ecosystem, predictable systemd behavior, and low-friction setup. While Arch Linux was my initial host OS, its rolling release model and AUR reliance make it too unstable for production infrastructure. Conversely, while RHEL is the gold standard for regulated enterprise data centers due to its 10-year lifecycle and compliance certifications, it introduces unnecessary subscription overhead for a simple internal platform.
Before provisioning the VM, QEMU and its dependencies were installed on the Arch Linux host:
Command:
sudo pacman -S qemu-full libvirt virt-manager dnsmasqIssue Encountered:
error: failed to prepare transaction (could not satisfy dependencies)
:: installing nettle (4.0-1) breaks dependency 'nettle=3.10.2' required by lib32-nettle
:: installing nettle (4.0-1) breaks dependency 'libnettle.so=8-64' required by libcurl-gnutls
:: installing nettle (4.0-1) breaks dependency 'libnettle.so=8-64' required by wget
Resolution: Added the -Syu flag to force a full system sync and upgrade alongside the installation, resolving all dependency conflicts.
Post-install configuration:
sudo systemctl enable --now libvirtd
sudo virsh net-start default
sudo virsh net-autostart default
sudo usermod -aG libvirt $USERThe Ubuntu ISO was then downloaded from the official Ubuntu website and a VM was provisioned with 6 GB RAM and 4 CPU cores.
Objective: Create an application user and secure a designated directory.
sudo useradd -s /usr/sbin/nologin appsvc
id appsvcOutput:
uid=1001(appsvc) gid=1001(appsvc) groups=1001(appsvc)
The -s /usr/sbin/nologin flag ensures the account cannot be used for interactive login, making it a service-only user.
sudo mkdir -p /opt/platform-status
sudo chown appsvc:appsvc /opt/platform-status
sudo chmod 750 /opt/platform-status
sudo bash -c 'echo "This directory holds the files for the internal platform status page." > /opt/platform-status/README.txt'Evidence — /opt listing:
ls -la /opttotal 12
drwxr-xr-x 3 root root 4096 Jun 3 15:48 .
drwxr-xr-x 20 root root 4096 Jun 3 15:41 ..
drwxr-x--- 2 appsvc appsvc 4096 Jun 3 15:53 platform-status
Evidence — directory contents (sudo required since we are not logged in as appsvc):
sudo ls -la /opt/platform-statustotal 12
drwxr-x--- 2 appsvc appsvc 4096 Jun 3 15:53 .
drwxr-xr-x 3 root root 4096 Jun 3 15:48 ..
-rw-r--r-- 1 appsvc appsvc 34 Jun 3 15:53 README.txt
Note on Permissions (750): This octal value ensures the Owner (appsvc) has full Read/Write/Execute permissions (7), the Group (appsvc) has Read/Execute permissions (5), and all other unauthorized users have zero access (0).
Objective: Inspect network state, secure connectivity, and establish services.
ip addr1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:51:15:02 brd ff:ff:ff:ff:ff:ff
altname enx525400511502
inet 169.254.239.207/16 brd 169.254.255.255 scope link noprefixroute enp1s0
valid_lft forever preferred_lft forever
inet6 fe80::5054:ff:fe51:1502/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever
The address 169.254.x.x is a link-local fallback, meaning the interface failed to obtain an address from the DHCP server. To resolve this, the interface name was identified and a static IP was manually assigned within the virtual network's subnet.
nmcli connection showNAME UUID TYPE DEVICE
netplan-enp1s0 cac41fbe-bc18-3d87-bba7-af2af7f8ffab ethernet enp1s0
lo c09a2266-c82d-4224-9c0e-f203fc46a720 loopback lo
sudo nmcli connection modify "netplan-enp1s0" ipv4.addresses 192.168.122.50/24 ipv4.gateway 192.168.122.1 ipv4.dns "8.8.8.8,192.168.122.1" ipv4.method manualsudo nmcli connection up "netplan-enp1s0"Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/3)
ip routedefault via 192.168.122.1 dev enp1s0 proto static metric 20100
192.168.122.0/24 dev enp1s0 proto kernel scope link src 192.168.122.50 metric 100
The static IP 192.168.122.50 was successfully configured with gateway 192.168.122.1 (sourced from QEMU network settings).
Running ss -tlpen before SSH installation revealed no port 22:
ss -tlpenState Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.1:631 0.0.0.0:* ino:16662 sk:8 cgroup:/system.slice/system-cups.slice/cups.service <->
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* uid:989 ino:4912 sk:9 cgroup:/system.slice/systemd-resolved.service <->
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* uid:989 ino:4914 sk:a cgroup:/system.slice/systemd-resolved.service <->
LISTEN 0 4096 [::1]:631 [::]:* ino:16661 sk:b cgroup:/system.slice/system-cups.slice/cups.service v6only:1 <->
SSH was not listening, so it was installed and enabled:
sudo apt update && sudo apt install openssh-server -y
sudo systemctl enable --now sshRunning ss -tlpen again confirmed port 22 was now open:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* uid:989 ino:7030 sk:1 cgroup:/system.slice/systemd-resolved.service <->
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* uid:989 ino:7032 sk:2 cgroup:/system.slice/systemd-resolved.service <->
LISTEN 0 4096 127.0.0.1:631 0.0.0.0:* ino:17571 sk:3 cgroup:/system.slice/system-cups.slice/cups.service <->
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* ino:27294 sk:5 cgroup:/system.slice/ssh.socket <->
LISTEN 0 4096 [::1]:631 [::]:* ino:17570 sk:4 cgroup:/system.slice/system-cups.slice/cups.service v6only:1 <->
LISTEN 0 4096 [::]:22 [::]:* ino:24553 sk:6 cgroup:/system.slice/ssh.socket v6only:1 <->
curl was installed to test outbound connectivity:
sudo apt install curl
curl -Iv https://google.com* Host google.com:443 was resolved.
* IPv6: 2a00:1450:4006:809::200e
* IPv4: 172.217.20.110
* Trying [2a00:1450:4006:809::200e]:443...
* connect to 2a00:1450:4006:809::200e port 443 from fec0::59dd:fbc6:7bb0:4c8e port 38416 failed: Network is unreachable
* Trying 172.217.20.110:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* SSL Trust Anchors:
* CAfile: /etc/ssl/certs/ca-certificates.crt
* CApath: /etc/ssl/certs
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
* subject: CN=*.google.com
* start date: May 7 15:51:26 2026 GMT
* expire date: Jul 30 15:51:25 2026 GMT
* issuer: C=US; O=Google Trust Services; CN=WR2
* Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using sha256WithRSAEncryption
* Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Certificate level 2: Public key type RSA (4096/152 Bits/secBits), signed using sha384WithRSAEncryption
* subjectAltName: "google.com" matches cert's "google.com"
* SSL certificate verified via OpenSSL.
* Established connection to google.com (172.217.20.110 port 443) from 10.0.2.15 port 60808
* using HTTP/2
...
< HTTP/2 301
The TLS handshake completed successfully via IPv4. IPv6 was unreachable within the VM network, but the connection fell back gracefully and succeeded.
traceroute was not pre-installed and was installed before use:
traceroute 8.8.8.8traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
1 _gateway (10.0.2.2) 0.226 ms 0.211 ms 0.203 ms
2 _gateway (192.168.100.1) 0.668 ms 0.657 ms 0.818 ms
3 10.14.128.1 9.870 ms 10.014 ms 10.044 ms
4 10.119.9.130 20.375 ms 20.364 ms 20.376 ms
5 172.30.10.130 6.377 ms 6.236 ms 6.589 ms
6 151.248.98.152 6.760 ms 5.861 ms 6.091 ms
7 151.248.105.50 54.537 ms 54.856 ms 54.834 ms
8 142.250.166.24 129.459 ms 129.356 ms 129.552 ms
9 192.178.105.209 58.630 ms * 58.277 ms
10 142.251.78.85 63.039 ms 72.14.233.39 55.481 ms 142.251.78.83 262.874 ms
11 dns.google (8.8.8.8) 63.225 ms 63.804 ms 62.802 ms
The first hop outside the local virtual environment was hop 3 (10.14.128.1, ISP private routing). The first true hop to the public internet, outside the ISP's private network, occurred at hop 6 (151.248.98.152).
- TCP (Transmission Control Protocol): A connection-oriented, highly reliable protocol. It establishes a formal connection via a 3-way handshake and guarantees that all data arrives in order by requiring the receiver to acknowledge every packet. The tradeoff is added latency and overhead.
- UDP (User Datagram Protocol): A connectionless, unreliable protocol. It prioritizes speed by blasting data to the receiver without checking if it arrived successfully. Ideal for live video streaming or gaming where speed matters more than perfect delivery.
Objective: Create an executable script to monitor and log system parameters.
sudo nano /usr/local/bin/healthcheck.sh
sudo chmod +x /usr/local/bin/healthcheck.shTesting the Disk Threshold:
To verify the >80% disk space warning functioned correctly, the script was temporarily edited to inject a mock variable (DISK_USAGE=99). The script successfully evaluated the condition and printed:
WARNING: Disk usage is above 80%!
Objective: Automate the script to run every 5 minutes using systemd.
Configuration Files Created:
/etc/systemd/system/healthcheck.service
[Unit]
Description=System Healthcheck Automation
[Service]
Type=oneshot
ExecStart=/usr/local/bin/healthcheck.shType=oneshot tells systemd the script runs once and exits, rather than running as a persistent daemon.
/etc/systemd/system/healthcheck.timer
[Unit]
Description=Run System Healthcheck Every 5 Minutes
[Timer]
# Run 1 minute after boot
OnBootSec=1min
# Run every 5 minutes thereafter
OnUnitActiveSec=5min
[Install]
WantedBy=timers.targetActivation:
sudo systemctl daemon-reload
sudo systemctl enable healthcheck.timer
sudo systemctl start healthcheck.timerVerification — timer status:
systemctl list-timers --all | grep healthcheckFri 2026-06-05 16:22:37 +03 4min 38s Fri 2026-06-05 16:17:37 +03 21s ago healthcheck.timer healthcheck.service
systemctl status healthcheck.timer● healthcheck.timer - Run System Healthcheck Every 5 Minutes
Loaded: loaded (/etc/systemd/system/healthcheck.timer; enabled; preset: enabled)
Active: active (waiting) since Fri 2026-06-05 16:17:37 +03; 1min 32s ago
Invocation: 1714c636f87c45859b6b71d180ca15f9
Trigger: Fri 2026-06-05 16:22:37 +03; 3min 27s left
Triggers: ● healthcheck.service
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 systemd[1]: Started healthcheck.timer - Run System Healthcheck Every 5 Minutes.
Journal Logs:
journalctl -u healthcheck.service -n 30 --no-pagerJun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 systemd[1]: Starting healthcheck.service - System Healthcheck Automation...
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: ===== System Healthcheck =====
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: Host: husam-Standard-PC-Q35-ICH9-2009
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: Timestamp: 2026-06-05 16:17:37
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: Uptime: up 18 minutes
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: Load Average: 1.25 0.54 0.34
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: CPU: 30.2% used
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: Memory: total=5302MB used=1594MB free=638MB
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: Disk: 57% used
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: OK
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: Top memory processes:
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: PID COMMAND %MEM
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: 2027 gnome-shell 6.9
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: 5372 ptyxis 4.5
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 healthcheck.sh[7483]: 6686 unattended-upgr 3.7
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 systemd[1]: healthcheck.service: Deactivated successfully.
Jun 05 16:17:37 husam-Standard-PC-Q35-ICH9-2009 systemd[1]: Finished healthcheck.service - System Healthcheck Automation.
Objective: Deploy an Nginx web server via Docker mapping to port 8080.
Installation & setup:
sudo apt install docker.io -y
sudo systemctl enable --now dockerCreating the HTML page:
mkdir -p ~/docker
echo "<h1>Internal Status Page</h1>
<p>Name: Husam</p>
<p>Hostname: $(hostname)</p>
<p>Date: $(date)</p>" > ~/docker/index.htmlRunning the container:
sudo docker run -d \
--name status-page \
-p 8080:80 \
--restart always \
-v ~/docker/index.html:/usr/share/nginx/html/index.html:ro \
nginx:alpineEvidence — container running:
sudo docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
641ee0ad7342 nginx:alpine "/docker-entrypoint.…" 34 seconds ago Up 32 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp status-page
Evidence — page served correctly:
curl http://localhost:8080<h1>Internal Status Page</h1>
<p>Name: Husam</p>
<p>Hostname: husam-Standard-PC-Q35-ICH9-2009</p>
<p>Date: Fri Jun 5 04:28:29 PM +03 2026</p>Docker Concepts:
- Image vs. Container: An image is a read-only blueprint containing the OS and application dependencies (analogous to a class in OOP). A container is the live, running instance spawned from that blueprint (analogous to an object).
- Bind Mount vs. Volume: A bind mount maps a specific file or folder from the host directly into the container — used above to inject the HTML file. A volume is an isolated storage space managed entirely by Docker, best suited for persistent data such as databases.
- Port Conflicts: If port 8080 were already in use, the conflicting process would be identified with
ss -tlpen | grep 8080, then either killed, stopped, or worked around by remapping the container to a different host port (e.g.,-p 8081:80).
Mistakes and Troubleshooting:
- Arch Linux Dependency Issue: While installing QEMU on the Arch host, a dependency conflict arose involving
libnettle.so=8-64. This was resolved by using thepacman -Syuflag to force a full system sync and upgrade alongside the installation.
What I Had To Look Up: The correct package name for Docker on Ubuntu and how to set up QEMU/KVM from scratch.
Future Automations: If this were a real production environment, all manual terminal steps would be eliminated by provisioning the VM using Terraform with the QEMU/libvirt provider, turning the entire infrastructure into version-controlled, repeatable code.
The system is fully operational. A final review confirms:
ss -tlpenshows ports 22 (SSH) and 8080 (Docker/Nginx) actively listening.systemctl list-timersshowshealthcheck.timerticking down and firing every 5 minutes.curl localhost:8080returns the dynamically generated HTML status page.