<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Urvin Sanghavi</title>
    <description>The latest articles on DEV Community by Urvin Sanghavi (@urvin).</description>
    <link>https://dev.to/urvin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3984063%2F7c048fb0-4524-4396-8697-74ef7fe5bfa1.png</url>
      <title>DEV Community: Urvin Sanghavi</title>
      <link>https://dev.to/urvin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/urvin"/>
    <language>en</language>
    <item>
      <title>I built a daily work log for Claude Code, and it never leaves my machines</title>
      <dc:creator>Urvin Sanghavi</dc:creator>
      <pubDate>Sat, 26 Sep 2026 18:02:21 +0000</pubDate>
      <link>https://dev.to/urvin/i-built-a-daily-work-log-for-claude-code-and-it-never-leaves-my-machines-45on</link>
      <guid>https://dev.to/urvin/i-built-a-daily-work-log-for-claude-code-and-it-never-leaves-my-machines-45on</guid>
      <description>&lt;p&gt;I use Claude Code for most of my day, on a laptop and a desktop, with a work account and a personal one. At 6 pm I often couldn't answer two simple questions: what did I actually get done today, and how much did it use?&lt;/p&gt;

&lt;p&gt;Token counters answer the second question. I wanted an answer to the first, so I built &lt;strong&gt;worklog-for-claude-code&lt;/strong&gt;, a small open-source tool for Linux.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F44twf5r05cjjhm18p7in.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F44twf5r05cjjhm18p7in.png" alt="Daily view" width="800" height="759"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it gives you
&lt;/h2&gt;

&lt;p&gt;Once a day you get a simple page with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you worked on&lt;/strong&gt;: a short summary for each project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens used&lt;/strong&gt;: per day, per account and per model, with a 14-day chart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything together&lt;/strong&gt;: all your computers and Claude accounts in one view&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After setup it runs in the background. You don't have to remember anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Claude Code writes a local transcript for every session. The tool reads those transcripts in two ways: through Claude Code hooks that fire when a session starts and ends, and through an hourly systemd timer that catches anything the hooks missed.&lt;/p&gt;

&lt;p&gt;For each session it writes one small JSON record into a git repo you own. Every file path is written by exactly one device, so several machines can push to the same repo without merge conflicts. A nightly job turns the records into a Markdown report, and a local web UI shows it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fus-urvin%2Fworklog-for-claude-code%2Fmain%2Fdocs%2Fimages%2Fhow-it-works.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fus-urvin%2Fworklog-for-claude-code%2Fmain%2Fdocs%2Fimages%2Fhow-it-works.svg" alt="How it works" width="960" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy was the first requirement
&lt;/h2&gt;

&lt;p&gt;Transcripts contain everything you typed, so the tool is built to keep as little as possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It stores your first prompts in each session, &lt;strong&gt;cut to 300 characters&lt;/strong&gt;, along with token counts, file names and your own commit subjects.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;never&lt;/strong&gt; stores file contents, Claude's replies, tool output or credentials.&lt;/li&gt;
&lt;li&gt;Before anything is written, it removes API keys, tokens, JWTs, private keys, passwords in URLs, e-mail addresses and IP addresses.&lt;/li&gt;
&lt;li&gt;There is no server and no telemetry. Data stays on your machine and in your private repo.&lt;/li&gt;
&lt;li&gt;AI summaries are optional and run through your own &lt;code&gt;claude -p&lt;/code&gt;. Turn them off to keep everything fully local.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The web UI listens on loopback only, and checks the Host and Origin headers, uses CSRF tokens and sends a strict Content-Security-Policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 30 seconds
&lt;/h2&gt;

&lt;p&gt;This runs the UI on fake data and doesn't touch your files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx run &lt;span class="nt"&gt;--spec&lt;/span&gt; worklog-for-claude-code claude-worklog demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;a href="http://localhost:8766" rel="noopener noreferrer"&gt;http://localhost:8766&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To install it for real:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;worklog-for-claude-code
git init ~/claude-worklog-data
claude-worklog install-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://worklog.localhost:8765" rel="noopener noreferrer"&gt;http://worklog.localhost:8765&lt;/a&gt;, fill in the short setup form and click &lt;strong&gt;Install hook&lt;/strong&gt;. For several machines, put the data folder in a private GitHub or GitLab repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux with systemd only&lt;/strong&gt; for now. A macOS port would need launchd units, and contributions are welcome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code only&lt;/strong&gt;, not claude.ai chat, because chats aren't written to local transcripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The transcript format is unofficial&lt;/strong&gt;, so a Claude Code update could break parsing for a while. The parser skips lines it doesn't understand instead of crashing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens are not dollars.&lt;/strong&gt; On Pro and Max plans usage counts against limits, so no prices are shown.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/us-urvin/worklog-for-claude-code" rel="noopener noreferrer"&gt;https://github.com/us-urvin/worklog-for-claude-code&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PyPI: &lt;a href="https://pypi.org/project/worklog-for-claude-code/" rel="noopener noreferrer"&gt;https://pypi.org/project/worklog-for-claude-code/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's MIT licensed and version 0.1.0. If you try it, I'd love to hear what's missing.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Automatically Deploy Your App to a VPS with GitHub Actions</title>
      <dc:creator>Urvin Sanghavi</dc:creator>
      <pubDate>Fri, 11 Sep 2026 00:04:23 +0000</pubDate>
      <link>https://dev.to/urvin/automatically-deploy-your-app-to-a-vps-with-github-actions-4k5o</link>
      <guid>https://dev.to/urvin/automatically-deploy-your-app-to-a-vps-with-github-actions-4k5o</guid>
      <description>&lt;p&gt;If your deploy process is "SSH into the server, &lt;code&gt;cd&lt;/code&gt; to the app, &lt;code&gt;git pull&lt;/code&gt;, restart something, hope" — this post replaces it with &lt;code&gt;git push&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll have by the end:&lt;/strong&gt; every push to your &lt;code&gt;main&lt;/code&gt; branch updates the app on your server automatically. You can also trigger a deploy by hand from the GitHub UI.&lt;/p&gt;

&lt;p&gt;This is the &lt;em&gt;simple&lt;/em&gt; version — it has a few seconds of downtime during the restart and it builds on the server. &lt;a href="https://dev.toPASTE_URL_HERE"&gt;Post 6 in this series&lt;/a&gt; upgrades it to a zero-downtime setup. Start here; the simple version is enough for a long time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is "deploying with GitHub Actions"?
&lt;/h2&gt;

&lt;p&gt;GitHub Actions is a task runner built into every GitHub repo. You describe a job in a YAML file, and GitHub runs it on a fresh virtual machine when something happens — in our case, "when code lands on &lt;code&gt;main&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;The job we want is tiny: connect to our server over SSH and run the same commands we'd run by hand. The value isn't magic — it's that the steps are &lt;em&gt;written down&lt;/em&gt;, &lt;em&gt;always run in the same order&lt;/em&gt;, and &lt;em&gt;don't depend on you being awake&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An app in a GitHub repo.&lt;/li&gt;
&lt;li&gt;A VPS (any provider) you can SSH into, with your app already cloned and running once — e.g. in &lt;code&gt;/srv/snip&lt;/code&gt;, served by nginx, run by a process manager. If you don't have that yet, set it up manually first; this post automates the &lt;em&gt;update&lt;/em&gt;, not the first install.&lt;/li&gt;
&lt;li&gt;A dedicated Linux user for deploys (don't use &lt;code&gt;root&lt;/code&gt;). We'll call it &lt;code&gt;deploy&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Make an SSH key just for deploys
&lt;/h2&gt;

&lt;p&gt;On your &lt;strong&gt;local machine&lt;/strong&gt;, generate a key pair that only GitHub Actions will use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/snip_deploy &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"github-actions-deploy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates &lt;code&gt;~/.ssh/snip_deploy&lt;/code&gt; (private) and &lt;code&gt;~/.ssh/snip_deploy.pub&lt;/code&gt; (public).&lt;/p&gt;

&lt;p&gt;Add the &lt;strong&gt;public&lt;/strong&gt; key to your server so the &lt;code&gt;deploy&lt;/code&gt; user accepts it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-copy-id &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/snip_deploy.pub deploy@YOUR_SERVER_IP
&lt;span class="c"&gt;# or manually: append the .pub line to /home/deploy/.ssh/authorized_keys on the server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/snip_deploy deploy@YOUR_SERVER_IP &lt;span class="s2"&gt;"echo connected"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Give the deploy user permission to restart the app
&lt;/h2&gt;

&lt;p&gt;The deploy needs to restart your app process without a password prompt. If you run the app as a systemd service (&lt;code&gt;snip.service&lt;/code&gt;), allow exactly that one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On the server, as root:&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart snip, /usr/bin/systemctl reload nginx'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/sudoers.d/snip-deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Adjust for your stack:&lt;/strong&gt; if you use PM2, supervisor, Docker Compose, etc., substitute the restart command you actually run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Store the secrets in GitHub
&lt;/h2&gt;

&lt;p&gt;In your repo: &lt;strong&gt;Settings → Secrets and variables → Actions → New repository secret&lt;/strong&gt;. Add three:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VPS_HOST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;your server's IP or hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VPS_USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deploy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VPS_SSH_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the &lt;strong&gt;entire contents&lt;/strong&gt; of &lt;code&gt;~/.ssh/snip_deploy&lt;/code&gt; (the private key, including the &lt;code&gt;BEGIN&lt;/code&gt;/&lt;code&gt;END&lt;/code&gt; lines)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Secrets are encrypted and never printed in logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: The workflow file
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="c1"&gt;# adds a "Run workflow" button in the Actions tab&lt;/span&gt;

&lt;span class="c1"&gt;# Never run two deploys at once.&lt;/span&gt;
&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-main&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to VPS&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy over SSH&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v1.2.2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_SSH_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;set -euo pipefail&lt;/span&gt;
            &lt;span class="s"&gt;cd /srv/snip&lt;/span&gt;

            &lt;span class="s"&gt;# Get the exact code that's on origin/main.&lt;/span&gt;
            &lt;span class="s"&gt;git fetch origin main&lt;/span&gt;
            &lt;span class="s"&gt;git reset --hard origin/main&lt;/span&gt;

            &lt;span class="s"&gt;# Install dependencies and build.&lt;/span&gt;
            &lt;span class="s"&gt;# Adjust for your stack:&lt;/span&gt;
            &lt;span class="s"&gt;npm ci&lt;/span&gt;
            &lt;span class="s"&gt;npm run build&lt;/span&gt;

            &lt;span class="s"&gt;# Apply database migrations.&lt;/span&gt;
            &lt;span class="s"&gt;# Adjust for your stack:&lt;/span&gt;
            &lt;span class="s"&gt;npm run migrate&lt;/span&gt;

            &lt;span class="s"&gt;# Restart the app.&lt;/span&gt;
            &lt;span class="s"&gt;sudo systemctl restart snip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit and push it to &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What each part does
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;on.push.branches: [main]&lt;/code&gt;&lt;/strong&gt; — run only when commits land on &lt;code&gt;main&lt;/code&gt;. Pull requests don't trigger it. (&lt;a href="https://dev.toPASTE_URL_HERE"&gt;Post 2&lt;/a&gt; adds a test job that &lt;em&gt;does&lt;/em&gt; run on pull requests.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;concurrency&lt;/code&gt;&lt;/strong&gt; — if you push twice quickly, the second deploy waits for the first to finish instead of racing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git reset --hard origin/main&lt;/code&gt;&lt;/strong&gt; — not &lt;code&gt;git pull&lt;/code&gt;. This guarantees the server's code is &lt;em&gt;exactly&lt;/em&gt; &lt;code&gt;origin/main&lt;/code&gt;, even if something on the server got modified. Nothing should ever be edited directly on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;set -euo pipefail&lt;/code&gt;&lt;/strong&gt; — stop on the first error instead of charging ahead. If &lt;code&gt;npm run build&lt;/code&gt; fails, the deploy stops &lt;em&gt;before&lt;/em&gt; the restart, and your old version keeps running.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Watch it run
&lt;/h2&gt;

&lt;p&gt;Push any commit to &lt;code&gt;main&lt;/code&gt;, then open the repo's &lt;strong&gt;Actions&lt;/strong&gt; tab. You'll see the workflow run live. Click it to see the SSH output.&lt;/p&gt;

&lt;p&gt;To deploy without a new commit (e.g. to retry): &lt;strong&gt;Actions → Deploy → Run workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Permission denied (publickey)&lt;/code&gt;&lt;/strong&gt; — the private key in &lt;code&gt;VPS_SSH_KEY&lt;/code&gt; doesn't match the public key in &lt;code&gt;authorized_keys&lt;/code&gt;, or you pasted only part of it. Re-copy the whole file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;could not read Username for 'https://github.com'&lt;/code&gt;&lt;/strong&gt; — your server's clone uses an HTTPS remote, which can't authenticate non-interactively. Switch it to SSH with a &lt;a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh" rel="noopener noreferrer"&gt;deploy key&lt;/a&gt;, or make the repo public, or add a step that sets a token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;npm: command not found&lt;/code&gt;&lt;/strong&gt; — the non-interactive SSH session has a minimal &lt;code&gt;PATH&lt;/code&gt;. Use full paths, or add &lt;code&gt;source ~/.profile&lt;/code&gt; / &lt;code&gt;source ~/.nvm/nvm.sh&lt;/code&gt; at the top of the script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build gets killed on a small server&lt;/strong&gt; — &lt;code&gt;npm run build&lt;/code&gt; can run out of memory on a 1 GB VPS. Add swap, or build in GitHub Actions and copy the result over — that's covered in &lt;a href="https://dev.toPASTE_URL_HERE"&gt;post 2&lt;/a&gt; and &lt;a href="https://dev.toPASTE_URL_HERE"&gt;post 6&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The site is broken for a few seconds on every deploy&lt;/strong&gt; — expected with this approach. &lt;a href="https://dev.toPASTE_URL_HERE"&gt;Post 6&lt;/a&gt; fixes it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Right now a commit that breaks the app will still deploy — the workflow doesn't know the difference between working code and broken code. &lt;a href="https://dev.toPASTE_URL_HERE"&gt;Next: run your test suite in CI and only deploy if it passes.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>githubactions</category>
      <category>cicd</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
