<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ardiansyah's Dev Notes]]></title><description><![CDATA[Practical notes, hands-on experiments, and learnings on full-stack web development, Linux server setup, Docker, and self-hosting by Ardiansyah Sulistyo.]]></description><link>https://ardiansyahsulistyo.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Ardiansyah&apos;s Dev Notes</title><link>https://ardiansyahsulistyo.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 10 Sep 2026 05:47:16 GMT</lastBuildDate><atom:link href="https://ardiansyahsulistyo.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Minimal monitoring: logs, disk, and uptime on a small Ubuntu VPS]]></title><description><![CDATA[This is the fifth and (for now) final post in a small series on baseline Ubuntu VPS setup before deploying web applications. The previous posts covered:

Baseline server setup: non-root user, SSH keys]]></description><link>https://ardiansyahsulistyo.hashnode.dev/minimal-monitoring-logs-disk-uptime-ubuntu-vps</link><guid isPermaLink="true">https://ardiansyahsulistyo.hashnode.dev/minimal-monitoring-logs-disk-uptime-ubuntu-vps</guid><category><![CDATA[Ubuntu]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Linux]]></category><category><![CDATA[webdevelopment]]></category><category><![CDATA[monitoring]]></category><dc:creator><![CDATA[Ardiansyah]]></dc:creator><pubDate>Sat, 05 Sep 2026 08:41:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a9baa797ccc83763ea2c0d6/4a5b234e-58b3-4838-9283-ddf6acc79062.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the fifth and (for now) final post in a small series on baseline Ubuntu VPS setup before deploying web applications. The previous posts covered:</p>
<ul>
<li>Baseline server setup: non-root user, SSH keys, firewall, automatic updates.</li>
<li>Serving a small web app with Nginx + PHP-FPM or Node reverse proxy.</li>
<li>Background jobs with Supervisor and cron for Laravel.</li>
<li>Backups for database and files, including offsite storage and restore testing.</li>
</ul>
<p>At this point, the app is running, jobs are processing, and backups are in place. The last piece is knowing when something goes wrong.</p>
<p>This is how I do minimal monitoring on small Ubuntu VPS instances: checking logs, watching disk usage, and setting up basic uptime monitoring. It's not a full observability stack. It's just enough so I'm not completely blind when issues happen.</p>
<h2>What this covers (and what it doesn't)</h2>
<p>This post focuses on:</p>
<ul>
<li>Checking application and system logs.</li>
<li>Monitoring disk usage and basic resources.</li>
<li>Simple uptime monitoring with external services.</li>
</ul>
<p>This is not:</p>
<ul>
<li>A Prometheus + Grafana setup.</li>
<li>A distributed tracing or metrics platform.</li>
<li>A complete monitoring solution for large teams.</li>
</ul>
<p>If your app is small enough that a single VPS makes sense, this pattern is usually enough to notice problems before users complain.</p>
<h2>Logs that matter</h2>
<p>For a typical small web app, I care about:</p>
<h3>Nginx logs</h3>
<p>Usually in:</p>
<pre><code class="language-bash">/var/log/nginx/myapp-access.log
/var/log/nginx/myapp-error.log
</code></pre>
<p>I use these to:</p>
<ul>
<li>See incoming requests and response codes.</li>
<li>Debug 5xx errors and upstream issues.</li>
</ul>
<p>Quick checks:</p>
<pre><code class="language-bash">sudo tail -f /var/log/nginx/myapp-error.log
sudo tail -n 100 /var/log/nginx/myapp-access.log | grep " 5[0-9][0-9] "
</code></pre>
<h3>Application logs</h3>
<p>For Laravel, typically:</p>
<pre><code class="language-bash">/var/www/myapp/storage/logs/laravel.log
</code></pre>
<p>For Node apps, wherever you configure your logger to write.</p>
<p>I look for:</p>
<ul>
<li>Exceptions and stack traces.</li>
<li>Repeated errors around the same time as user reports.</li>
<li>Patterns that suggest a specific endpoint or job is failing.</li>
</ul>
<h3>System logs</h3>
<p>On Ubuntu:</p>
<pre><code class="language-bash">/var/log/syslog
</code></pre>
<p>or via <code>journalctl</code>:</p>
<pre><code class="language-bash">sudo journalctl -u nginx --since "1 hour ago"
sudo journalctl -u supervisor --since "1 hour ago"
</code></pre>
<p>These help when:</p>
<ul>
<li>A service fails to start.</li>
<li>There are OOM kills or disk issues.</li>
<li>SSH or system-level problems occur.</li>
</ul>
<h2>Making logs easier to read</h2>
<p>A few habits that help me:</p>
<ul>
<li><p><strong>Use <code>less</code> for large logs</strong>  </p>
<pre><code class="language-bash">sudo less /var/log/nginx/myapp-error.log
</code></pre>
<p>Then search with <code>/</code> inside <code>less</code>.</p>
</li>
<li><p><strong>Filter by time or pattern</strong>  </p>
<pre><code class="language-bash">sudo grep "2025-09-05" /var/www/myapp/storage/logs/laravel.log | less
sudo grep -i "timeout" /var/log/nginx/myapp-error.log
</code></pre>
</li>
<li><p><strong>Follow logs in real time when debugging</strong>  </p>
<pre><code class="language-bash">sudo tail -f /var/log/nginx/myapp-error.log /var/www/myapp/storage/logs/laravel.log
</code></pre>
</li>
</ul>
<p>I don't set up centralized logging for small VPS instances. Instead, I make sure I can quickly SSH in and read logs when needed.</p>
<h2>Monitoring disk usage</h2>
<p>Running out of disk is a common way for small VPS instances to die quietly.</p>
<p>Basic checks:</p>
<pre><code class="language-bash">df -h
</code></pre>
<p>Look at:</p>
<ul>
<li>Overall usage on <code>/</code>.</li>
<li>Any partitions or mount points that are close to 100%.</li>
</ul>
<p>Find large directories:</p>
<pre><code class="language-bash">sudo du -sh /var/www/* | sort -h
sudo du -sh /var/log/* | sort -h
</code></pre>
<p>For logs, I rely on:</p>
<ul>
<li><strong>Logrotate</strong> (usually already configured on Ubuntu).</li>
<li>Occasional manual cleanup if a specific log grows too large.</li>
</ul>
<p>Example logrotate config for a custom app log:</p>
<pre><code class="language-bash">sudo nano /etc/logrotate.d/myapp
</code></pre>
<pre><code class="language-text">/var/www/myapp/storage/logs/laravel.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    create 0644 deploy deploy
}
</code></pre>
<p>This keeps 7 days of logs, compressed after the first day.</p>
<h2>Basic resource checks</h2>
<p>For CPU and RAM, I usually stick to simple tools:</p>
<pre><code class="language-bash">htop
top
free -h
</code></pre>
<p>I don't install heavy monitoring agents on small VPS instances unless there's a clear need. Instead:</p>
<ul>
<li>Check <code>htop</code> when debugging slowness.</li>
<li>Look for processes using unexpected CPU or memory.</li>
<li>Correlate with logs to see if a specific job or endpoint is the culprit.</li>
</ul>
<p>If you want something slightly more structured without going full metrics stack, tools like <code>glances</code> or <code>ncdu</code> can be helpful, but they're optional.</p>
<h2>Uptime monitoring from the outside</h2>
<p>Internal checks are useful, but if the server is completely down, you can't SSH in to run them. That's why I use external uptime monitoring.</p>
<p>Options I've used:</p>
<ul>
<li>Uptime Kuma (self-hosted).</li>
<li>StatusCake, Pingdom, UptimeRobot, or similar services.</li>
<li>Simple cron-based scripts that hit an endpoint and send email on failure.</li>
</ul>
<p>Typical setup:</p>
<ul>
<li>Create an HTTP check for <code>https://example.com</code>.</li>
<li>Set check interval (e.g., every 1–5 minutes).</li>
<li>Configure email or webhook notifications.</li>
</ul>
<p>This gives you:</p>
<ul>
<li>Alert when the site is down.</li>
<li>Basic uptime history.</li>
<li>A way to notice outages you might otherwise miss.</li>
</ul>
<p>For small projects, this is often more valuable than a complex internal metrics system.</p>
<h2>A simple routine</h2>
<p>Instead of trying to watch everything all the time, I use a light routine:</p>
<h3>Daily (optional, mostly automated)</h3>
<ul>
<li>Uptime monitoring runs automatically.</li>
<li>Backups run via cron (from part 4).</li>
<li>Logs rotate automatically.</li>
</ul>
<h3>Weekly or biweekly (manual, 5–10 minutes)</h3>
<p>When I remember or have time:</p>
<pre><code class="language-bash">df -h
sudo du -sh /var/www/* | sort -h
sudo tail -n 50 /var/log/nginx/myapp-error.log
sudo tail -n 50 /var/www/myapp/storage/logs/laravel.log
</code></pre>
<p>I look for:</p>
<ul>
<li>Disk creeping towards full.</li>
<li>Repeated errors in logs.</li>
<li>Any unusual patterns.</li>
</ul>
<h3>After incidents</h3>
<p>When something goes wrong:</p>
<ul>
<li>Check logs around the incident time.</li>
<li>Note what happened and what fixed it.</li>
<li>Adjust backups, logs, or monitoring if needed.</li>
</ul>
<p>Over time, this builds a lightweight but practical understanding of how the server behaves.</p>
<h2>Common mistakes I keep making</h2>
<p>These are the issues I run into most often:</p>
<ul>
<li><p><strong>Ignoring disk until it's full</strong><br />Not checking <code>df -h</code> until the site starts failing with "no space left on device."</p>
</li>
<li><p><strong>Not reading error logs</strong><br />Relying only on "the site works" without ever checking Nginx or app error logs.</p>
</li>
<li><p><strong>No external uptime check</strong><br />Only discovering the server is down when a user reports it.</p>
</li>
<li><p><strong>Logs growing forever</strong><br />Forgetting to configure logrotate for custom logs, leading to huge files.</p>
</li>
<li><p><strong>Over-engineering monitoring</strong><br />Installing heavy tooling on a 1 GB VPS, then spending more time maintaining monitoring than the app itself.</p>
</li>
</ul>
<p>For small VPS instances, I prefer boring, simple tools that I actually use over fancy setups I'll ignore.</p>
<h2>What this is not</h2>
<p>This approach is not:</p>
<ul>
<li>A replacement for proper alerting in critical production systems.</li>
<li>A design for high-traffic or distributed architectures.</li>
<li>A complete security monitoring solution.</li>
</ul>
<p>It's just enough monitoring so I'm not completely blind when something breaks on a small VPS.</p>
<h2>Wrapping up the series</h2>
<p>With this post, the baseline series covers:</p>
<ol>
<li>Baseline server setup (users, SSH, firewall, updates).</li>
<li>Serving apps with Nginx + PHP/Node.</li>
<li>Background jobs with Supervisor and cron.</li>
<li>Backups for database and files, including offsite storage and restore testing.</li>
<li>Minimal monitoring: logs, disk, and uptime.</li>
</ol>
<p>This is not a complete production checklist for every scenario, but it's the baseline I want in place before considering a small app "properly deployed" on a VPS.</p>
<h2>Before you consider this done</h2>
<p>Before I stop thinking about monitoring on a small VPS, I make sure:</p>
<ul>
<li>I know where the main logs live and how to read them.</li>
<li>Disk usage is checked occasionally, and logrotate is configured.</li>
<li>There's at least one external uptime check with notifications.</li>
<li>I have a simple routine (even if irregular) to glance at logs and resources.</li>
</ul>
<p>If you've followed this series, you now have a repeatable baseline for deploying small web apps to Ubuntu VPS instances. From here, you can iterate based on your actual needs instead of starting from vague best-practice checklists.</p>
]]></content:encoded></item><item><title><![CDATA[Backups that actually work: database & files on a small Ubuntu VPS]]></title><description><![CDATA[This is the fourth post in a small series on baseline Ubuntu VPS setup before deploying web applications. The previous posts covered:

Baseline server setup: non-root user, SSH keys, firewall, automat]]></description><link>https://ardiansyahsulistyo.hashnode.dev/backups-that-actually-work-database-files-ubuntu-vps</link><guid isPermaLink="true">https://ardiansyahsulistyo.hashnode.dev/backups-that-actually-work-database-files-ubuntu-vps</guid><category><![CDATA[Ubuntu]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Linux]]></category><category><![CDATA[webdevelopment]]></category><category><![CDATA[Backup]]></category><dc:creator><![CDATA[Ardiansyah]]></dc:creator><pubDate>Sat, 05 Sep 2026 08:32:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a9baa797ccc83763ea2c0d6/ab35ad36-0beb-4f5b-9552-6a27a59f7d35.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the fourth post in a small series on baseline Ubuntu VPS setup before deploying web applications. The previous posts covered:</p>
<ul>
<li>Baseline server setup: non-root user, SSH keys, firewall, automatic updates.</li>
<li>Serving a small web app with Nginx + PHP-FPM or Node reverse proxy.</li>
<li>Background jobs with Supervisor and cron for Laravel.</li>
</ul>
<p>Now that the app is running and processing jobs, the next question is: what happens if this VPS disappears tomorrow?</p>
<p>This is how I handle backups for small web apps on Ubuntu VPS: database dumps, important files, offsite storage, and actually testing restores. It's not a disaster recovery plan for a large company. It's just the minimal setup I want before considering an app "deployed."</p>
<h2>What this covers (and what it doesn't)</h2>
<p>This post focuses on:</p>
<ul>
<li>Backing up MySQL/MariaDB or PostgreSQL databases.</li>
<li>Backing up important application files (uploads, <code>.env</code>, etc.).</li>
<li>Storing backups outside the VPS.</li>
<li>Testing restores so you know the backups are not useless.</li>
</ul>
<p>This is not:</p>
<ul>
<li>A complete backup strategy for large teams or compliance-heavy environments.</li>
<li>A guide to encrypted backup pipelines with key management.</li>
<li>A replacement for proper monitoring and alerting.</li>
</ul>
<p>If your app is small enough that a single VPS makes sense, this pattern is usually enough to sleep better at night.</p>
<h2>What to back up</h2>
<p>For a typical small Laravel or web app, I back up:</p>
<h3>Database</h3>
<ul>
<li>The main application database.</li>
<li>Any additional databases used by the app (if applicable).</li>
</ul>
<h3>Files</h3>
<p>At minimum:</p>
<ul>
<li><code>.env</code> (or equivalent config with secrets).</li>
<li><code>storage/</code> directory (uploads, logs that matter).</li>
<li>Any other directory where user-generated content lives.</li>
</ul>
<p>Code itself is often in Git, so I don't always back it up separately, but for very small projects I sometimes include the entire <code>/var/www/myapp</code> directory for simplicity.</p>
<h2>A simple backup script</h2>
<p>I usually create a small shell script that:</p>
<ul>
<li>Dumps the database.</li>
<li>Archives important files.</li>
<li>Names backups with timestamps.</li>
<li>Stores them in a dedicated directory (e.g., <code>/root/backups</code>).</li>
</ul>
<p>Example:</p>
<pre><code class="language-bash">sudo mkdir -p /root/backups
sudo nano /root/backups/myapp-backup.sh
</code></pre>
<p>A minimal script might look like this:</p>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

APP_NAME="myapp"
BACKUP_DIR="/root/backups"
DATE=$(date +%Y-%m-%d-%H%M%S)

DB_USER="myapp_user"
DB_NAME="myapp_db"
DB_HOST="localhost"

# Database backup
mysqldump -u"$DB_USER" -p"${DB_PASSWORD:-}" -h"$DB_HOST" "$DB_NAME" \
  | gzip &gt; "$BACKUP_DIR/${APP_NAME}-db-${DATE}.sql.gz"

# Files backup
tar -czf "$BACKUP_DIR/${APP_NAME}-files-${DATE}.tar.gz" \
  -C /var/www myapp \
  --exclude=myapp/vendor \
  --exclude=myapp/node_modules

# Rotate old backups (keep last 7 days)
find "$BACKUP_DIR" -name "${APP_NAME}-*.sql.gz" -mtime +7 -delete
find "$BACKUP_DIR" -name "${APP_NAME}-*.tar.gz" -mtime +7 -delete
</code></pre>
<p>Adjust:</p>
<ul>
<li>Database credentials and command (<code>mysqldump</code> vs <code>pg_dump</code>).</li>
<li>Paths and exclusions for your app.</li>
<li>Retention period (here: 7 days).</li>
</ul>
<p>Make it executable:</p>
<pre><code class="language-bash">sudo chmod +x /root/backups/myapp-backup.sh
</code></pre>
<p>Test it manually once:</p>
<pre><code class="language-bash">sudo /root/backups/myapp-backup.sh
ls -lh /root/backups
</code></pre>
<p>Check that:</p>
<ul>
<li>Files are created.</li>
<li>Sizes look reasonable.</li>
<li>No obvious errors in the output.</li>
</ul>
<h2>Storing backups offsite</h2>
<p>Keeping backups only on the same VPS is better than nothing, but not by much. If the VPS is lost or compromised, you lose both app and backups.</p>
<p>Common options for offsite storage:</p>
<ul>
<li>Object storage: S3, Backblaze B2, Cloudflare R2, etc.</li>
<li>Another server you control via SCP/rsync.</li>
<li>A backup service that pulls from your server.</li>
</ul>
<p>For object storage, tools like <code>rclone</code> or provider-specific CLI tools work well. A very simple pattern:</p>
<ul>
<li>Run the backup script locally.</li>
<li>Sync the <code>/root/backups</code> directory to remote storage.</li>
</ul>
<p>Example with <code>rclone</code> (conceptual):</p>
<pre><code class="language-bash">rclone sync /root/backups remote-bucket:myapp-backups
</code></pre>
<p>I won't go into full <code>rclone</code> setup here; the key point is:</p>
<ul>
<li>Backups should live outside the VPS.</li>
<li>The process should be automated, not something you remember to do manually.</li>
</ul>
<h2>Automating backups with cron</h2>
<p>Once the script works manually, add a cron job.</p>
<p>Edit root's crontab:</p>
<pre><code class="language-bash">sudo crontab -e
</code></pre>
<p>Add something like:</p>
<pre><code class="language-cron">0 2 * * * /root/backups/myapp-backup.sh &gt;&gt; /var/log/myapp-backup.log 2&gt;&amp;1
</code></pre>
<p>This runs the backup daily at 02:00.</p>
<p>Check logs occasionally:</p>
<pre><code class="language-bash">tail /var/log/myapp-backup.log
</code></pre>
<p>Look for:</p>
<ul>
<li>Successful completion messages.</li>
<li>Any errors from <code>mysqldump</code>, <code>tar</code>, or the upload step.</li>
</ul>
<h2>Testing restores (the part everyone skips)</h2>
<p>A backup you haven't tested is not a backup; it's hope.</p>
<p>At least once, ideally on a schedule, test restoring from your backups.</p>
<h3>Restore database</h3>
<p>On a test server or the same server (if you're comfortable):</p>
<pre><code class="language-bash"># Decompress
gunzip &lt; /root/backups/myapp-db-2025-09-05-020000.sql.gz &gt; restore.sql

# Import
mysql -u"$DB_USER" -p"${DB_PASSWORD:-}" -h"$DB_HOST" "$DB_NAME" &lt; restore.sql
</code></pre>
<p>Then:</p>
<ul>
<li>Check that tables exist.</li>
<li>Run a few queries to confirm data looks right.</li>
<li>If possible, run the app against this restored database in a staging environment.</li>
</ul>
<h3>Restore files</h3>
<pre><code class="language-bash">cd /var/www
tar -xzf /root/backups/myapp-files-2025-09-05-020000.tar.gz
</code></pre>
<p>Check:</p>
<ul>
<li>Permissions on <code>storage/</code> and other directories.</li>
<li>That recent uploads are present.</li>
<li>That <code>.env</code> matches the environment (or adjust as needed).</li>
</ul>
<h3>Document the restore steps</h3>
<p>I keep a short note somewhere (could be a private README) with:</p>
<ul>
<li>Where backups are stored.</li>
<li>How to restore database and files.</li>
<li>Any gotchas specific to the app.</li>
</ul>
<p>This makes future restores (by you or someone else) much less stressful.</p>
<h2>Common mistakes I keep making</h2>
<p>These are the issues I run into most often:</p>
<ul>
<li><p><strong>No offsite copy</strong><br />Backups exist, but only on the same VPS. If the VPS dies, backups die with it.</p>
</li>
<li><p><strong>Never testing restores</strong><br />Backups run daily, but no one has ever tried to restore from them.</p>
</li>
<li><p><strong>Wrong permissions after restore</strong><br />Files are restored, but <code>storage/</code> is not writable by the web user, causing silent failures.</p>
</li>
<li><p><strong>Ignoring logs</strong><br />Backup cron runs, but errors are never checked; backups have been failing for weeks.</p>
</li>
<li><p><strong>Backing up too much or too little</strong><br />Either huge backups that are slow and expensive, or missing critical directories like <code>storage/app</code>.</p>
</li>
</ul>
<p>When in doubt, I re-check:</p>
<pre><code class="language-bash">ls -lh /root/backups
tail /var/log/myapp-backup.log
</code></pre>
<p>and occasionally run a restore on a test server.</p>
<h2>What this is not</h2>
<p>This setup is not:</p>
<ul>
<li>A replacement for proper monitoring and alerting on backup jobs.</li>
<li>A design for multi-region, compliance-heavy environments.</li>
<li>A guarantee that you'll never lose data.</li>
</ul>
<p>It's just a minimal baseline so that if the VPS disappears, you can rebuild without starting from zero.</p>
<h2>What comes next</h2>
<p>With backups in place, the last core piece in this series is minimal monitoring:</p>
<ul>
<li>Checking logs.</li>
<li>Watching disk usage.</li>
<li>Basic uptime monitoring.</li>
</ul>
<p>That's the topic for the next post in this series.</p>
<h2>Before you move on</h2>
<p>Before considering this step "done", I make sure:</p>
<ul>
<li>A backup script exists and runs automatically.</li>
<li>Backups are stored outside the VPS.</li>
<li>I have successfully restored both database and files at least once.</li>
<li>I know where to find backup logs and how to troubleshoot failures.</li>
</ul>
<p>This isn't a complete disaster recovery plan, but it's the baseline I want before moving on to monitoring.</p>
]]></content:encoded></item><item><title><![CDATA[Background jobs the simple way: Supervisor & cron for Laravel on Ubuntu VPS]]></title><description><![CDATA[This is the third post in a small series on baseline Ubuntu VPS setup before deploying web applications. The first two posts covered:

Baseline server setup: non-root user, SSH keys, firewall, automat]]></description><link>https://ardiansyahsulistyo.hashnode.dev/background-jobs-supervisor-cron-laravel-ubuntu-vps</link><guid isPermaLink="true">https://ardiansyahsulistyo.hashnode.dev/background-jobs-supervisor-cron-laravel-ubuntu-vps</guid><category><![CDATA[Ubuntu]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Linux]]></category><category><![CDATA[webdevelopment]]></category><dc:creator><![CDATA[Ardiansyah]]></dc:creator><pubDate>Sat, 05 Sep 2026 08:22:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a9baa797ccc83763ea2c0d6/65288d64-9922-43cd-9b55-cbeb434add08.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the third post in a small series on baseline Ubuntu VPS setup before deploying web applications. The first two posts covered:</p>
<ul>
<li>Baseline server setup: non-root user, SSH keys, firewall, automatic updates.</li>
<li>Serving a small web app with Nginx + PHP-FPM or Node reverse proxy.</li>
</ul>
<p>Now that the app is reachable over HTTP, the next boring-but-important piece is background jobs and scheduled tasks.</p>
<p>This is how I run Laravel queue workers and scheduled commands on a small Ubuntu VPS using Supervisor and cron. It's not a distributed queue architecture or a monitoring deep dive. It's just the setup I actually use for small to medium projects.</p>
<h2>What this covers (and what it doesn't)</h2>
<p>This post focuses on:</p>
<ul>
<li>Running Laravel queue workers with Supervisor.</li>
<li>Running Laravel's scheduler with a simple cron entry.</li>
<li>Basic checks to see if things are actually working.</li>
</ul>
<p>This is not:</p>
<ul>
<li>A guide to Redis clusters or horizontal scaling.</li>
<li>A complete monitoring and alerting setup.</li>
<li>A comparison of all possible queue drivers and tools.</li>
</ul>
<p>If your app is small enough that a single VPS makes sense, this pattern is usually more than enough.</p>
<h2>Why Supervisor and cron</h2>
<p>For small Laravel apps, I typically use:</p>
<ul>
<li><strong>Supervisor</strong> to keep queue workers running.</li>
<li><strong>Cron</strong> to trigger Laravel's scheduler every minute.</li>
</ul>
<p>Reasons:</p>
<ul>
<li>Supervisor is simple, stable, and widely available on Ubuntu.</li>
<li>Cron is already there; no need to introduce systemd timers or external schedulers for basic use cases.</li>
<li>This combination is easy to debug and document.</li>
</ul>
<p>I don't use this setup for massive scale, but for most side projects and small production apps, it works reliably.</p>
<h2>Installing Supervisor</h2>
<p>On a fresh Ubuntu server:</p>
<pre><code class="language-bash">sudo apt update
sudo apt install supervisor
</code></pre>
<p>Check that it's running:</p>
<pre><code class="language-bash">systemctl status supervisor
</code></pre>
<p>If the service is active, you can move on to configuring a worker for your app.</p>
<h2>Directory and user assumptions</h2>
<p>This guide assumes:</p>
<ul>
<li>App code lives in <code>/var/www/myapp</code>.</li>
<li>The app is owned by the <code>deploy</code> user (the same non-root user from part 1).</li>
<li>PHP CLI version matches your stack (e.g., <code>php8.2</code>).</li>
</ul>
<p>Adjust paths and versions to match your environment.</p>
<h2>Basic queue configuration in Laravel</h2>
<p>In <code>.env</code>, I usually have something like:</p>
<pre><code class="language-env">QUEUE_CONNECTION=database
</code></pre>
<p>or, if using Redis:</p>
<pre><code class="language-env">QUEUE_CONNECTION=redis
</code></pre>
<p>For this post, the driver details don't matter much; the Supervisor config is similar either way. What matters is that:</p>
<ul>
<li>Jobs are being dispatched correctly.</li>
<li>A worker process is running to handle them.</li>
</ul>
<h2>Supervisor config for a Laravel queue worker</h2>
<p>Supervisor configs typically live in <code>/etc/supervisor/conf.d/</code>.</p>
<p>I create a file like:</p>
<pre><code class="language-bash">sudo nano /etc/supervisor/conf.d/myapp-worker.conf
</code></pre>
<p>A minimal config looks like this:</p>
<pre><code class="language-ini">[program:myapp-worker]
command=/usr/bin/php8.2 /var/www/myapp/artisan queue:work database --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasuser=false
killasgroup=true
user=deploy
numprocs=1
redirect_stderr=true
stdout_logfile=/var/log/supervisor/myapp-worker.out.log
stderr_logfile=/var/log/supervisor/myapp-worker.err.log
</code></pre>
<p>Key points:</p>
<ul>
<li><code>command</code> points to your <code>artisan</code> file and desired queue driver.</li>
<li><code>--max-time</code> helps avoid memory leaks by restarting workers periodically.</li>
<li><code>user=deploy</code> runs the worker as the same user that owns the app files.</li>
<li>Logs are written under <code>/var/log/supervisor/</code>.</li>
</ul>
<p>Create the log directory if it doesn't exist:</p>
<pre><code class="language-bash">sudo mkdir -p /var/log/supervisor
sudo chown deploy:deploy /var/log/supervisor
</code></pre>
<p>Adjust permissions as needed for your setup.</p>
<h2>Enabling and starting the worker</h2>
<p>After saving the config:</p>
<pre><code class="language-bash">sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start myapp-worker
</code></pre>
<p>Check status:</p>
<pre><code class="language-bash">sudo supervisorctl status
</code></pre>
<p>You should see something like:</p>
<pre><code class="language-text">myapp-worker                   RUNNING   pid 12345, uptime 0:00:10
</code></pre>
<p>If it fails to start, check:</p>
<ul>
<li>The <code>command</code> path is correct.</li>
<li>PHP CLI is installed and matches the version in the command.</li>
<li>The <code>deploy</code> user can execute <code>artisan queue:work</code>.</li>
</ul>
<h2>Running the scheduler with cron</h2>
<p>Laravel's scheduler is designed to be triggered by a single cron entry that runs every minute.</p>
<p>Edit the crontab for the <code>deploy</code> user:</p>
<pre><code class="language-bash">crontab -e
</code></pre>
<p>Add:</p>
<pre><code class="language-cron">* * * * * cd /var/www/myapp &amp;&amp; /usr/bin/php8.2 artisan schedule:run &gt;&gt; /dev/null 2&gt;&amp;1
</code></pre>
<p>Notes:</p>
<ul>
<li>Use the full path to <code>php</code> (e.g., <code>/usr/bin/php8.2</code>) to avoid environment issues.</li>
<li>Redirecting output to <code>/dev/null</code> keeps cron quiet; logs should be handled in your Laravel code or <code>schedule:run</code> options if needed.</li>
</ul>
<p>This single line is enough to enable all scheduled tasks defined in <code>app/Console/Kernel.php</code> (or the relevant scheduler file for your Laravel version).</p>
<h2>Testing that jobs actually run</h2>
<p>It's easy to configure Supervisor and cron and still have nothing actually working. I usually run a few quick checks:</p>
<h3>1. Dispatch a test job</h3>
<p>From the app directory:</p>
<pre><code class="language-bash">cd /var/www/myapp
php8.2 artisan tinker
</code></pre>
<p>Then dispatch a simple job, or trigger something in your app that you know should create a job (e.g., send an email, process an upload).</p>
<h3>2. Check Supervisor logs</h3>
<pre><code class="language-bash">sudo tail -f /var/log/supervisor/myapp-worker.err.log
sudo tail -f /var/log/supervisor/myapp-worker.out.log
</code></pre>
<p>Look for:</p>
<ul>
<li>Job processing messages.</li>
<li>Any exceptions or connection errors.</li>
</ul>
<h3>3. Check scheduler execution</h3>
<p>To verify cron is running the scheduler:</p>
<ul>
<li>Add a temporary scheduled command in Laravel that writes to a log or file every minute.</li>
<li>Wait a couple of minutes.</li>
<li>Check that the file/log is updated.</li>
</ul>
<p>Once confirmed, remove or disable the test command.</p>
<h2>Common mistakes I keep making</h2>
<p>These are the issues I run into most often:</p>
<ul>
<li><p><strong>Wrong PHP version</strong><br />Config uses <code>php8.2</code>, but the system default is <code>8.1</code> or <code>8.3</code>, or CLI isn't installed.</p>
</li>
<li><p><strong>Wrong working directory</strong><br /><code>command</code> doesn't <code>cd</code> into the app directory, causing path issues for some jobs.</p>
</li>
<li><p><strong>Permissions</strong><br />The <code>deploy</code> user can run <code>artisan</code>, but some jobs need write access to <code>storage/</code> or <code>bootstrap/cache/</code>.</p>
</li>
<li><p><strong>Cron environment differences</strong><br />Cron runs with a minimal environment; relying on <code>.env</code> loading via Composer or framework bootstrapping is fine, but assuming shell aliases or custom PATH is not.</p>
</li>
<li><p><strong>Assuming "RUNNING" means "working"</strong><br />Supervisor can show <code>RUNNING</code> while the worker is stuck or failing repeatedly. Logs are the source of truth.</p>
</li>
</ul>
<p>When something feels off, I always go back to:</p>
<pre><code class="language-bash">sudo supervisorctl status
sudo tail -n 50 /var/log/supervisor/myapp-worker.err.log
grep -i error /var/log/supervisor/myapp-worker.err.log
</code></pre>
<h2>What this is not</h2>
<p>This setup is not:</p>
<ul>
<li>A replacement for proper error handling and logging in your jobs.</li>
<li>A monitoring solution. You still need to check logs and, ideally, have some form of alerting.</li>
<li>A design for high-scale distributed systems.</li>
</ul>
<p>It's just a reliable baseline for small apps where a single VPS is enough.</p>
<h2>What comes next</h2>
<p>With the app served by Nginx and background jobs running, the next important piece is backups:</p>
<ul>
<li>Database dumps.</li>
<li>Important files (uploads, <code>.env</code>, etc.).</li>
<li>Storing backups outside the VPS.</li>
<li>Actually testing restores.</li>
</ul>
<p>That's the topic for the next post in this series.</p>
<h2>Before you move on</h2>
<p>Before considering this step "done", I make sure:</p>
<ul>
<li>A queue worker is running under Supervisor and processing jobs.</li>
<li>The scheduler is triggering every minute via cron.</li>
<li>I know where to find worker logs and how to restart the worker.</li>
<li>I have at least one real job running in production, not just a test command.</li>
</ul>
<p>This isn't a complete job architecture, but it's the baseline I want before moving on to backups and monitoring.</p>
]]></content:encoded></item><item><title><![CDATA[Serving a small web app: Nginx + PHP/Node on Ubuntu VPS]]></title><description><![CDATA[This is the second post in a small series on baseline Ubuntu VPS setup before deploying web applications. The first post covered the server baseline: non-root user, SSH keys, firewall, and automatic u]]></description><link>https://ardiansyahsulistyo.hashnode.dev/serving-small-web-app-nginx-ubuntu-vps</link><guid isPermaLink="true">https://ardiansyahsulistyo.hashnode.dev/serving-small-web-app-nginx-ubuntu-vps</guid><category><![CDATA[Ubuntu]]></category><category><![CDATA[nginx]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Linux]]></category><category><![CDATA[webdevelopment]]></category><dc:creator><![CDATA[Ardiansyah]]></dc:creator><pubDate>Sat, 05 Sep 2026 07:52:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a9baa797ccc83763ea2c0d6/c9619e59-a57b-44a2-bb20-eb395ad74530.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the second post in a small series on baseline Ubuntu VPS setup before deploying web applications. The first post covered the server baseline: non-root user, SSH keys, firewall, and automatic updates.</p>
<p>Now that the server itself is in a sane state, the next step is getting a small web app in front of users without over-engineering the stack.</p>
<p>This is how I typically set up Nginx for small Laravel or Node apps on a fresh Ubuntu VPS. It's not a high-traffic architecture or a complete hardening guide. It's just the default pattern I reach for when the goal is "get this app online reliably."</p>
<h2>Why Nginx, and what this is not</h2>
<p>I use Nginx as a reverse proxy and/or web server because:</p>
<ul>
<li><p>It's lightweight and stable on small VPS instances.</p>
</li>
<li><p>It handles TLS termination cleanly.</p>
</li>
<li><p>It works well with both PHP-FPM and Node apps.</p>
</li>
</ul>
<p>This post is not:</p>
<ul>
<li><p>A performance tuning guide for high-traffic sites.</p>
</li>
<li><p>A complete security hardening reference.</p>
</li>
<li><p>A comparison of Nginx vs Apache vs Caddy.</p>
</li>
</ul>
<p>It's just the setup I actually run for small projects.</p>
<h2>Directory structure and ownership</h2>
<p>Before touching Nginx, I decide where the app will live and who owns it.</p>
<p>For most small apps, I use something like:</p>
<pre><code class="language-bash">/var/www/myapp
</code></pre>
<p>with ownership:</p>
<pre><code class="language-bash">sudo chown -R deploy:deploy /var/www/myapp
</code></pre>
<p>I avoid putting app code in home directories because:</p>
<ul>
<li><p>It keeps system users and app code conceptually separate.</p>
</li>
<li><p>It's easier to reason about in Nginx configs and backup scripts.</p>
</li>
</ul>
<p>The <code>deploy</code> user here is the same non-root sudo user from the first post in this series.</p>
<h2>Basic Nginx installation</h2>
<p>On a fresh Ubuntu server:</p>
<pre><code class="language-bash">sudo apt update
sudo apt install nginx
</code></pre>
<p>Then I check that Nginx is running:</p>
<pre><code class="language-bash">systemctl status nginx
</code></pre>
<p>At this point, Nginx is usually serving the default page on port 80. I leave that alone until I'm ready to switch traffic to the new site.</p>
<h2>Server block layout</h2>
<p>I manage sites using the standard Ubuntu pattern:</p>
<ul>
<li><p>Config file in <code>/etc/nginx/sites-available/myapp</code></p>
</li>
<li><p>Symlink in <code>/etc/nginx/sites-enabled/myapp</code></p>
</li>
</ul>
<p>Example:</p>
<pre><code class="language-bash">sudo nano /etc/nginx/sites-available/myapp
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp
sudo nginx -t
sudo systemctl reload nginx
</code></pre>
<p>I always run <code>nginx -t</code> before reloading. It's an easy way to catch syntax errors without taking down existing sites.</p>
<h2>Minimal server block for a Laravel (PHP-FPM) app</h2>
<p>For a typical Laravel app, my starting server block looks like this:</p>
<pre><code class="language-nginx">server {
    listen 80;
    server_name example.com www.example.com;

    root /var/www/myapp/public;
    index index.php index.html;

    access_log /var/log/nginx/myapp-access.log;
    error_log /var/log/nginx/myapp-error.log;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
}
</code></pre>
<p>Key points:</p>
<ul>
<li><p><code>root</code> points to the <code>public</code> directory, not the project root.</p>
</li>
<li><p><code>try_files</code> handles Laravel's front controller pattern.</p>
</li>
<li><p>PHP is passed to PHP-FPM via a Unix socket.</p>
</li>
<li><p>Logs are namespaced per app (<code>myapp-access.log</code>, <code>myapp-error.log</code>).</p>
</li>
</ul>
<p>Adjust the PHP version and socket path to match your installation (<code>php8.1-fpm.sock</code>, <code>php8.3-fpm.sock</code>, etc.).</p>
<h2>Minimal server block for a Node app (reverse proxy)</h2>
<p>For a Node app running on <code>localhost:3000</code> (or similar), the server block is even simpler:</p>
<pre><code class="language-nginx">server {
    listen 80;
    server_name example.com www.example.com;

    access_log /var/log/nginx/myapp-access.log;
    error_log /var/log/nginx/myapp-error.log;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
</code></pre>
<p>Notes:</p>
<ul>
<li><p><code>proxy_pass</code> points to the local Node process.</p>
</li>
<li><p>Headers like <code>X-Real-IP</code> and <code>X-Forwarded-For</code> help the app see the real client IP.</p>
</li>
<li><p>No PHP configuration is needed here.</p>
</li>
</ul>
<p>The Node process itself can be managed with systemd or PM2; that's a topic for a later post in this series.</p>
<h2>Enabling the site and testing configuration</h2>
<p>Once the server block is written:</p>
<pre><code class="language-bash">sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp
sudo nginx -t
</code></pre>
<p>If the test passes:</p>
<pre><code class="language-bash">sudo systemctl reload nginx
</code></pre>
<p>I then test with:</p>
<pre><code class="language-bash">curl -I http://example.com
</code></pre>
<p>and check:</p>
<ul>
<li><p>Status code (should be 200 or a redirect, not 500/502).</p>
</li>
<li><p>That the expected app is responding, not the default Nginx page.</p>
</li>
</ul>
<p>If something breaks, I check:</p>
<pre><code class="language-bash">sudo tail -f /var/log/nginx/myapp-error.log
</code></pre>
<p>while hitting the site in a browser.</p>
<h2>Common mistakes I keep making</h2>
<p>These are the issues I run into most often:</p>
<ul>
<li><p><strong>Wrong document root</strong><br />Pointing <code>root</code> at <code>/var/www/myapp</code> instead of <code>/var/www/myapp/public</code> for Laravel.</p>
</li>
<li><p><strong>PHP-FPM socket mismatch</strong><br />Config says <code>php8.2-fpm.sock</code>, but the system is running 8.1 or 8.3.</p>
</li>
<li><p><strong>Permissions</strong><br />Nginx can read files, but PHP-FPM runs as a different user and cannot write to <code>storage/</code> or <code>bootstrap/cache/</code>.</p>
</li>
<li><p><strong>server_name does not match</strong><br />DNS points to the server, but Nginx is still serving the default site because <code>server_name</code> doesn't match the request.</p>
</li>
<li><p><strong>Forgetting to reload</strong><br />Changing the config but forgetting <code>sudo systemctl reload nginx</code>.</p>
</li>
</ul>
<p>When in doubt, I re-check:</p>
<pre><code class="language-bash">sudo nginx -T | grep -A 10 'server_name example.com'
</code></pre>
<p>to see the effective configuration.</p>
<h2>What comes next</h2>
<p>At this point, the app is reachable over HTTP, but there are still some important pieces missing:</p>
<ul>
<li><p>Background jobs and scheduled tasks (queue workers, cron).</p>
</li>
<li><p>Backups for the database and important files.</p>
</li>
<li><p>Basic monitoring so I notice when something goes wrong.</p>
</li>
</ul>
<p>Those are the topics for the next posts in this series.</p>
<h2>Before you move on</h2>
<p>Before considering this step "done", I make sure:</p>
<ul>
<li><p>The app is accessible via the domain I expect.</p>
</li>
<li><p>Logs are being written to the expected files.</p>
</li>
<li><p>I can reproduce a request and see it in the access log.</p>
</li>
<li><p>I know which user runs PHP-FPM or the Node process, and which user owns the files.</p>
</li>
</ul>
<p>This is not a complete production checklist, but it's the baseline I want before moving on to queues, cron, and backups.</p>
]]></content:encoded></item><item><title><![CDATA[My first-pass checklist for a fresh Ubuntu VPS before deploying an app]]></title><description><![CDATA[When I deploy a small web app to a fresh Ubuntu VPS, getting the application running usually isn't the part I worry about most.
The stuff that's easy to miss is the server baseline: how I log in, whic]]></description><link>https://ardiansyahsulistyo.hashnode.dev/ubuntu-vps-first-pass-checklist</link><guid isPermaLink="true">https://ardiansyahsulistyo.hashnode.dev/ubuntu-vps-first-pass-checklist</guid><category><![CDATA[Devops]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Ubuntu]]></category><category><![CDATA[Security]]></category><category><![CDATA[self-hosted]]></category><dc:creator><![CDATA[Ardiansyah]]></dc:creator><pubDate>Sat, 05 Sep 2026 06:58:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a9baa797ccc83763ea2c0d6/5904bed5-dff2-4580-b3e8-c9478cc5c69c.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I deploy a small web app to a fresh Ubuntu VPS, getting the application running usually isn't the part I worry about most.</p>
<p>The stuff that's easy to miss is the server baseline: how I log in, which ports are open, whether I'm still using root, and whether the machine will keep getting security updates once I stop thinking about it for a few weeks.</p>
<p>This is the first-pass checklist I use before putting an app on a new VPS. It isn't a security audit or a promise that a server is fully secure. It's just the baseline I want in place before exposing an app to the internet.</p>
<h2>1. Update the system first</h2>
<pre><code class="language-shell">sudo apt update
sudo apt upgrade
</code></pre>
<p>I do this before installing the app stack so I'm not starting from an old package set. On an existing server, I check what will change first instead of blindly accepting upgrades.</p>
<p>If an update includes a new kernel, I check whether a reboot is needed:</p>
<pre><code class="language-shell">test -f /var/run/reboot-required &amp;&amp; echo "Reboot required"
</code></pre>
<h2>2. Create a non-root sudo user</h2>
<p>I don't use the root account as my normal deploy account.</p>
<pre><code class="language-shell">sudo adduser deploy
sudo usermod -aG sudo deploy
</code></pre>
<p>Before changing anything related to SSH, I open a second terminal and make sure the new user can log in and run sudo commands:</p>
<pre><code class="language-shell">ssh deploy@your-server-ip
sudo whoami
</code></pre>
<p>The expected output is:</p>
<pre><code class="language-shell">root
</code></pre>
<p>I keep the original root session open until this works. It's an easy way to avoid locking myself out halfway through the setup.</p>
<h2>3. Confirm SSH key login first</h2>
<p>On my local machine, I create a key if I don't already have one:</p>
<pre><code class="language-shell">ssh-keygen -t ed25519 -C "my-laptop"
</code></pre>
<p>Then I copy the public key to the new server:</p>
<pre><code class="language-shell">ssh-copy-id deploy@your-server-ip
</code></pre>
<p>I test key login in a new terminal before changing the SSH server configuration:</p>
<pre><code class="language-shell">ssh deploy@your-server-ip
</code></pre>
<p>Only after that works do I disable direct root login and password authentication.</p>
<h2>4. Harden SSH carefully</h2>
<p>On Ubuntu, I prefer adding a separate config file instead of editing the main SSH configuration directly:</p>
<pre><code class="language-shell">sudo nano /etc/ssh/sshd_config.d/99-hardening.conf
</code></pre>
<p>My basic starting point looks like this:</p>
<pre><code class="language-shell">PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
</code></pre>
<p>Before reloading SSH, I validate the configuration:</p>
<pre><code class="language-shell">sudo sshd -t
</code></pre>
<p>I also check the effective values, especially if the server already has other files under <code>sshd_config.d</code>:</p>
<pre><code class="language-shell">sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|pubkeyauthentication'
</code></pre>
<p>Then I reload the service:</p>
<pre><code class="language-shell">sudo systemctl reload ssh
</code></pre>
<p>I keep the current SSH session open and test a fresh login after the reload. If the new connection fails, the old session is still there to fix the configuration.</p>
<h2>5. Enable a basic firewall</h2>
<p>For a small web server, I normally start with a deny-by-default inbound policy:</p>
<pre><code class="language-shell">sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
</code></pre>
<p>If the server serves web traffic directly, I also allow HTTP and HTTPS:</p>
<pre><code class="language-shell">sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
</code></pre>
<p>Then I enable UFW and check the result:</p>
<pre><code class="language-shell">sudo ufw enable
sudo ufw status verbose
</code></pre>
<p>Don't enable UFW before allowing SSH. That's an easy way to lose access to a remote VPS. If I use a non-default SSH port, I allow that exact port instead of relying on the <code>OpenSSH</code> profile.</p>
<h2>6. Check automatic security updates</h2>
<p>On current Ubuntu Server releases, <code>unattended-upgrades</code> is usually already installed. I still check that automatic security updates are enabled and configured the way I expect:</p>
<pre><code class="language-shell">sudo dpkg-reconfigure unattended-upgrades
</code></pre>
<p>If the package is missing, I install it first:</p>
<pre><code class="language-shell">sudo apt install unattended-upgrades
</code></pre>
<p>I also check the logs from time to time:</p>
<pre><code class="language-shell">sudo ls -la /var/log/unattended-upgrades/
</code></pre>
<p>Automatic updates help avoid leaving known security fixes unapplied, but they don't replace backups, patch reviews, or monitoring.</p>
<h2>7. Install only what the app needs</h2>
<p>I try not to install a full collection of tools just because they're common in server setup guides.</p>
<p>For a typical web app, that might mean only:</p>
<ul>
<li><p>A reverse proxy such as Nginx or Caddy</p>
</li>
<li><p>The runtime I actually need, such as Node.js or PHP</p>
</li>
<li><p>A systemd service or Supervisor configuration for the app process</p>
</li>
<li><p>A database client only if the server needs one</p>
</li>
</ul>
<p>For Laravel deployments, I treat the Supervisor config and cron entry as part of the deployment, not something to add later after the first queue job gets stuck.</p>
<p>Every installed service is another thing to configure, patch, and monitor.</p>
<h2>8. Verify the baseline before deploying</h2>
<p>Before deploying the application, I check:</p>
<pre><code class="language-shell">whoami
sudo ufw status verbose
sudo systemctl status ssh
sudo ss -tulpn
</code></pre>
<p>I also make sure I know:</p>
<ul>
<li><p>Which user owns the application files</p>
</li>
<li><p>Which process starts the application</p>
</li>
<li><p>Which ports are intentionally exposed</p>
</li>
<li><p>Where logs will be written</p>
</li>
<li><p>How I would restore the application and its data if the VPS disappeared</p>
</li>
</ul>
<h2>What this checklist does not cover</h2>
<p>This is only a first pass. It does not replace:</p>
<ul>
<li><p>Application security reviews</p>
</li>
<li><p>Database backup and restore testing</p>
</li>
<li><p>Secret rotation</p>
</li>
<li><p>Dependency vulnerability management</p>
</li>
<li><p>DDoS protection</p>
</li>
<li><p>Monitoring and alerting strategy</p>
</li>
<li><p>Compliance requirements</p>
</li>
<li><p>A professional security audit</p>
</li>
</ul>
<p>A fresh VPS can be brought to a safer baseline fairly quickly, but security isn't a one-time command you run after provisioning.</p>
<h2>Before you deploy</h2>
<p>Before I put an app on a new VPS, I make sure:</p>
<ul>
<li><p>I can log in with a non-root user and an SSH key</p>
</li>
<li><p>Root and password SSH login are disabled only after key login is verified</p>
</li>
<li><p>The firewall allows only the ports the server actually needs</p>
</li>
<li><p>I know how the app starts, where it logs, and how I would restore its data</p>
</li>
</ul>
]]></content:encoded></item></channel></rss>