<?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: mkt</title>
    <description>The latest articles on DEV Community by mkt (@mkt).</description>
    <link>https://dev.to/mkt</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%2F3533376%2F53a2bbf3-6313-480b-bd79-92c15c8c0546.jpg</url>
      <title>DEV Community: mkt</title>
      <link>https://dev.to/mkt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mkt"/>
    <language>en</language>
    <item>
      <title>Restricting rsync to a Specific Directory with authorized_keys</title>
      <dc:creator>mkt</dc:creator>
      <pubDate>Tue, 03 Nov 2026 08:29:00 +0000</pubDate>
      <link>https://dev.to/mkt/restricting-rsync-to-a-specific-directory-with-authorizedkeys-5fom</link>
      <guid>https://dev.to/mkt/restricting-rsync-to-a-specific-directory-with-authorizedkeys-5fom</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In research environments, sharing large datasets with collaborators over the internet is a common task. This often involves transferring files with &lt;code&gt;scp&lt;/code&gt; (which uses &lt;code&gt;sftp&lt;/code&gt; under the hood these days) or &lt;code&gt;rsync&lt;/code&gt; over SSH with public key authentication. However, simply creating an SSH account for file sharing also allows access to other directories that the account can read or write, as well as shell access and port forwarding. That is not ideal from a security perspective.&lt;/p&gt;

&lt;p&gt;A common approach is to configure &lt;code&gt;ChrootDirectory&lt;/code&gt; for each user in &lt;code&gt;sshd_config&lt;/code&gt; and force the use of &lt;code&gt;internal-sftp&lt;/code&gt;. But for tasks such as synchronization, you may still want to use &lt;code&gt;rsync&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In this article, I'll show you a simple way to share files within a specific directory using SSH's &lt;code&gt;authorized_keys&lt;/code&gt; and the &lt;code&gt;rsync&lt;/code&gt; helper script &lt;code&gt;rrsync&lt;/code&gt; (restricted rsync).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note&lt;/p&gt;
&lt;p&gt;
This article assumes you are familiar with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating and configuring Linux users&lt;/li&gt;
&lt;li&gt;SSH public key authentication&lt;/li&gt;
&lt;li&gt;Basic &lt;code&gt;rsync&lt;/code&gt; usage&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SSH's &lt;code&gt;authorized_keys&lt;/code&gt; lets you restrict the commands and features available to each public key.&lt;/li&gt;
&lt;li&gt;Setting &lt;code&gt;rrsync&lt;/code&gt; as the &lt;code&gt;command&lt;/code&gt; in &lt;code&gt;authorized_keys&lt;/code&gt; lets you restrict the directory that a client can read from or write to using &lt;code&gt;rsync&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are examples of an &lt;code&gt;authorized_keys&lt;/code&gt; entry and an &lt;code&gt;rsync&lt;/code&gt; command. Adjust the username, public key, paths, command arguments, and other details to suit your environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Allow Read-Only Access
&lt;/h3&gt;

&lt;p&gt;An example &lt;code&gt;authorized_keys&lt;/code&gt; entry on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Allow read-only access to /srv/project-x on the server.

restrict,command="/usr/bin/rrsync -ro /srv/project-x" ssh-ed25519 AAAA...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An example &lt;code&gt;rsync&lt;/code&gt; command to run on the client:&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;# Download the files in /srv/project-x/dataset/ on the server to the client's mydataset directory.&lt;/span&gt;

user@client:~&lt;span class="nv"&gt;$ &lt;/span&gt;rsync &lt;span class="nt"&gt;-av&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'ssh -i ~/.ssh/id_ed25519'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    user@server:dataset/ &lt;span class="se"&gt;\&lt;/span&gt;
    ./mydataset/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path specified by the client is interpreted relative to &lt;code&gt;/srv/project-x&lt;/code&gt;, the directory configured on the server. For write-only access, use &lt;code&gt;-wo&lt;/code&gt; instead of &lt;code&gt;-ro&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Warning&lt;/p&gt;
&lt;p&gt;
Depending on the options you use, files may be transferred or deleted unintentionally, particularly when using &lt;code&gt;--delete&lt;/code&gt; for uploads. Apply appropriate restrictions in &lt;code&gt;authorized_keys&lt;/code&gt;, and check what will happen beforehand with &lt;code&gt;-n&lt;/code&gt; (&lt;code&gt;--dry-run&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Test Environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu Server 26.04&lt;/li&gt;
&lt;li&gt;rsync 3.4.1 (using the &lt;code&gt;rrsync&lt;/code&gt; script included in the &lt;code&gt;rsync&lt;/code&gt; package)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Warning&lt;/p&gt;
&lt;p&gt;
While writing this article, I noticed that the &lt;code&gt;rrsync&lt;/code&gt; script included with rsync 3.4.1 appears to have a vulnerability that allows directory restrictions to be bypassed (&lt;a href="https://github.com/RsyncProject/rsync/blob/master/NEWS.md#news-for-rsync-350-13-aug-2026" rel="noopener noreferrer"&gt;CVE-2026-53783&lt;/a&gt;). For safe use, use version 3.5.1 or later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  authorized_keys and rrsync
&lt;/h2&gt;

&lt;p&gt;Let's briefly look at &lt;code&gt;authorized_keys&lt;/code&gt; and &lt;code&gt;rrsync&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  authorized_keys
&lt;/h3&gt;

&lt;p&gt;To allow SSH login with public key authentication, add each client public key to the login user's &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; file on the server, one key per line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Example ~/.ssh/authorized_keys
ssh-ed25519 AAAA...(public-key1)
ssh-ed25519 AAAA...(public-key2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a line contains only a public key, the client can use a shell and run commands within the limits of the server configuration and the user's permissions.&lt;/p&gt;

&lt;p&gt;You can add options before each public key to restrict the features available when that key is used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;restrict,from="192.0.2.0/24",command="echo 'hello'" ssh-ed25519 AAAA...(public-key3)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;restrict&lt;/code&gt;: Disables PTY allocation, port forwarding, agent forwarding, X11 forwarding, and execution of &lt;code&gt;~/.ssh/rc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;from="..."&lt;/code&gt;: Restricts connections by source IP address or hostname. CIDR notation is also supported.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;command="..."&lt;/code&gt;: Runs the specified command instead of the command requested by the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the example above, PTY allocation and port forwarding are disabled, connections are allowed only from &lt;code&gt;192.0.2.0/24&lt;/code&gt;, and the command that runs is always &lt;code&gt;echo 'hello'&lt;/code&gt; (which simply prints hello), regardless of the command the user requests.&lt;/p&gt;

&lt;p&gt;There are many other options available in &lt;code&gt;authorized_keys&lt;/code&gt;. If you're interested, see the "AUTHORIZED_KEYS FILE FORMAT" section of the &lt;code&gt;sshd&lt;/code&gt; man page.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://manpages.ubuntu.com/manpages/resolute/man8/sshd.8.html" rel="noopener noreferrer"&gt;sshd(8) (Ubuntu 26.04)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  rrsync
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;rsync&lt;/code&gt; command synchronizes files between directories. Because it can synchronize data over SSH, it is commonly used for network backups and similar tasks.&lt;/p&gt;

&lt;p&gt;Although &lt;code&gt;rsync&lt;/code&gt; is often used to synchronize entire directories, you can also transfer individual files by specifying their paths.&lt;/p&gt;

&lt;p&gt;There is a helper script for &lt;code&gt;rsync&lt;/code&gt; called &lt;code&gt;rrsync&lt;/code&gt; (restricted rsync). It is implemented in Python and, as the name suggests, provides a restricted version of &lt;code&gt;rsync&lt;/code&gt;. On Ubuntu and some other distributions, the script is included in the &lt;code&gt;rsync&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;By setting &lt;code&gt;rrsync&lt;/code&gt; as the &lt;code&gt;command&lt;/code&gt; in &lt;code&gt;authorized_keys&lt;/code&gt;, you can make only a specific directory available to the client.&lt;/p&gt;

&lt;p&gt;Here is the help output for &lt;code&gt;rrsync&lt;/code&gt;:&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="nv"&gt;$ &lt;/span&gt;rrsync &lt;span class="nt"&gt;-help&lt;/span&gt;
usage: rrsync &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-ro&lt;/span&gt; | &lt;span class="nt"&gt;-wo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-munge&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-no-del&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-no-lock&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-no-overwrite&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-help&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; DIR

Use &lt;span class="s2"&gt;"man rrsync"&lt;/span&gt; to learn how to restrict ssh &lt;span class="nb"&gt;users &lt;/span&gt;to using a restricted rsync command.

positional arguments:
  DIR            The restricted directory to use.

options:
  &lt;span class="nt"&gt;-ro&lt;/span&gt;            Allow only reading from the DIR. Implies &lt;span class="nt"&gt;-no-del&lt;/span&gt; and &lt;span class="nt"&gt;-no-lock&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
  &lt;span class="nt"&gt;-wo&lt;/span&gt;            Allow only writing to the DIR.
  &lt;span class="nt"&gt;-munge&lt;/span&gt;         Enable rsync&lt;span class="s1"&gt;'s --munge-links on the server side.
  -no-del        Disable rsync'&lt;/span&gt;s &lt;span class="nt"&gt;--delete&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt; and &lt;span class="nt"&gt;--remove&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt; options.
  &lt;span class="nt"&gt;-no-lock&lt;/span&gt;       Avoid the single-run &lt;span class="o"&gt;(&lt;/span&gt;per-user&lt;span class="o"&gt;)&lt;/span&gt; lock check.
  &lt;span class="nt"&gt;-no-overwrite&lt;/span&gt;  Prevent overwriting existing files by enforcing &lt;span class="nt"&gt;--ignore-existing&lt;/span&gt;
  &lt;span class="nt"&gt;-help&lt;/span&gt;, &lt;span class="nt"&gt;-h&lt;/span&gt;      Output this &lt;span class="nb"&gt;help &lt;/span&gt;message and exit.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only required argument is &lt;code&gt;DIR&lt;/code&gt;, which specifies the directory to share.&lt;/p&gt;

&lt;p&gt;If you set &lt;code&gt;rrsync /srv/project-x&lt;/code&gt; as the &lt;code&gt;command&lt;/code&gt; in &lt;code&gt;authorized_keys&lt;/code&gt;, the client can use &lt;code&gt;rsync&lt;/code&gt; to read and write files only within &lt;code&gt;/srv/project-x/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With this configuration, paths specified by the client are interpreted as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path specified by the client&lt;/th&gt;
&lt;th&gt;Actual path on the server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user@server:./&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/srv/project-x/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user@server:dataset/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/srv/project-x/dataset/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user@server:dataset/file.bin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/srv/project-x/dataset/file.bin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use &lt;code&gt;-ro&lt;/code&gt; or &lt;code&gt;-wo&lt;/code&gt; to allow only reading or only writing, respectively.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-ro&lt;/code&gt;: Allows only reading from the server.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-wo&lt;/code&gt;: Allows only writing to the server. On its own, &lt;code&gt;-wo&lt;/code&gt; does not prevent existing files from being overwritten or deleted, so combine it with &lt;code&gt;-no-del&lt;/code&gt; or &lt;code&gt;-no-overwrite&lt;/code&gt; as needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The restrictions imposed by &lt;code&gt;rrsync&lt;/code&gt; apply on top of normal Linux file permissions. Also, &lt;code&gt;rrsync&lt;/code&gt; works by checking paths and options; it does not provide isolation like &lt;code&gt;chroot&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note&lt;/p&gt;
&lt;p&gt;
Keep the following points in mind when choosing the shared directory (&lt;code&gt;DIR&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not use the user's home directory as &lt;code&gt;DIR&lt;/code&gt;. If &lt;code&gt;DIR&lt;/code&gt; contains &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; or &lt;code&gt;~/.bashrc&lt;/code&gt; and you allow write access, the client could modify those files to bypass the restrictions.&lt;/li&gt;
&lt;li&gt;Access outside &lt;code&gt;DIR&lt;/code&gt; through symbolic links is also subject to restrictions. &lt;code&gt;rrsync&lt;/code&gt; is designed to reject options that dereference symbolic links, such as &lt;code&gt;--copy-links&lt;/code&gt;, as well as paths that resolve outside &lt;code&gt;DIR&lt;/code&gt;. However, versions affected by the vulnerability mentioned in the Test Environment section may allow these restrictions to be bypassed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  How rrsync Works
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;rrsync&lt;/code&gt; is a Python wrapper script for &lt;code&gt;rsync&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/RsyncProject/rsync/blob/master/support/rrsync" rel="noopener noreferrer"&gt;GitHub: RsyncProject / rsync&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command specified by &lt;code&gt;command&lt;/code&gt; in &lt;code&gt;authorized_keys&lt;/code&gt; receives the command string originally requested by the client in the &lt;code&gt;SSH_ORIGINAL_COMMAND&lt;/code&gt; environment variable. &lt;code&gt;rrsync&lt;/code&gt; parses this string and starts &lt;code&gt;rsync&lt;/code&gt; on the server with the appropriate restrictions.&lt;/p&gt;

&lt;p&gt;The main rules for handling paths are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A path with &lt;code&gt;..&lt;/code&gt; as a component is rejected.&lt;/li&gt;
&lt;li&gt;An absolute path starting with &lt;code&gt;/&lt;/code&gt; is rewritten relative to &lt;code&gt;DIR&lt;/code&gt;. For example, with the configuration above, &lt;code&gt;user@server:/dataset/&lt;/code&gt; refers to &lt;code&gt;/srv/project-x/dataset/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You could implement similar behavior by checking &lt;code&gt;SSH_ORIGINAL_COMMAND&lt;/code&gt; in your own script. Be careful, though: mistakes in path or option handling could allow the restrictions to be bypassed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative: SFTP + chroot
&lt;/h2&gt;

&lt;p&gt;As a side note...&lt;/p&gt;

&lt;p&gt;If you don't specifically need &lt;code&gt;rsync&lt;/code&gt;, you can also combine SFTP with &lt;code&gt;ChrootDirectory&lt;/code&gt; to restrict access to a particular directory. This is probably the more common approach. In that case, configure the restrictions in the server's &lt;code&gt;sshd_config&lt;/code&gt; rather than &lt;code&gt;authorized_keys&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# /etc/ssh/sshd_config
Match User user2
    ChrootDirectory /srv/project-x
    ForceCommand internal-sftp
    DisableForwarding yes
    PermitTTY no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ChrootDirectory&lt;/code&gt;: Sets &lt;code&gt;/srv/project-x&lt;/code&gt; as the root directory after login. The client sees it as &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ForceCommand internal-sftp&lt;/code&gt;: Prevents commands other than SFTP from running. SCP using SFTP under the hood is also allowed. For read-only access, use &lt;code&gt;internal-sftp -R&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The directory specified by &lt;code&gt;ChrootDirectory&lt;/code&gt; and all its parent directories must be owned by &lt;code&gt;root&lt;/code&gt; and must not be writable by anyone other than &lt;code&gt;root&lt;/code&gt;. If you want to allow uploads, create a separate subdirectory that the user can write to, such as &lt;code&gt;/srv/project-x/upload&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this article, I showed how to use SSH's &lt;code&gt;authorized_keys&lt;/code&gt; and &lt;code&gt;rrsync&lt;/code&gt; to restrict the directory a client can access with &lt;code&gt;rsync&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Beyond the features and commands covered here, there are plenty of useful tools and options that are easy to overlook. Try exploring what might work for your own use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://download.samba.org/pub/rsync/rsync.1" rel="noopener noreferrer"&gt;rsync(1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://download.samba.org/pub/rsync/rrsync.1" rel="noopener noreferrer"&gt;rrsync(1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.ubuntu.com/manpages/resolute/man8/sshd.8.html" rel="noopener noreferrer"&gt;sshd(8): AUTHORIZED_KEYS FILE FORMAT (Ubuntu 26.04)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/RsyncProject/rsync/blob/master/NEWS.md" rel="noopener noreferrer"&gt;rsync NEWS (including the fix for CVE-2026-53783)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/RsyncProject/rsync/blob/master/support/rrsync" rel="noopener noreferrer"&gt;rrsync implementation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Change History
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;2026/11/03: First version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Personal Blog: &lt;a href="https://mkt-sidenotes.dev/tech/2026-10-18-restricting-rsync-to-a-directory-with-authorized-keys/" rel="noopener noreferrer"&gt;https://mkt-sidenotes.dev/tech/2026-10-18-restricting-rsync-to-a-directory-with-authorized-keys/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>ssh</category>
      <category>rsync</category>
    </item>
    <item>
      <title>Moving Away from sudo su (Ubuntu 26.04, sudo-rs)</title>
      <dc:creator>mkt</dc:creator>
      <pubDate>Tue, 22 Sep 2026 07:07:54 +0000</pubDate>
      <link>https://dev.to/mkt/moving-away-from-sudo-su-ubuntu-2604-sudo-rs-480d</link>
      <guid>https://dev.to/mkt/moving-away-from-sudo-su-ubuntu-2604-sudo-rs-480d</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;sudo -i&lt;/code&gt; (or &lt;code&gt;sudo -s&lt;/code&gt;) instead of &lt;code&gt;sudo su&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Ubuntu 26.04, the traditional &lt;code&gt;sudo&lt;/code&gt; implementation has been replaced by &lt;code&gt;sudo-rs&lt;/code&gt;, and signal handling seems to have changed slightly. As a result, if your SSH connection drops while you are running &lt;code&gt;sudo su&lt;/code&gt;, the &lt;code&gt;sudo su&lt;/code&gt; processes can stay alive indefinitely...&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Noticed
&lt;/h2&gt;

&lt;p&gt;While setting up an Ubuntu Server 26.04 machine over SSH, I happened to look at the process tree in &lt;code&gt;htop&lt;/code&gt; and noticed several &lt;code&gt;sudo su&lt;/code&gt; processes running.&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%2Fc6uadglpcawn8nqo87xj.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%2Fc6uadglpcawn8nqo87xj.png" alt="Process tree showing sudo su processes left running directly under PID 1" width="799" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;None of them were running under &lt;code&gt;tmux&lt;/code&gt;; they were all children of PID 1.&lt;/p&gt;

&lt;p&gt;What was going on...?&lt;/p&gt;

&lt;p&gt;I decided to investigate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Investigating the Cause
&lt;/h2&gt;

&lt;p&gt;Here's the environment I used for testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu Server 26.04&lt;/li&gt;
&lt;li&gt;sudo-rs (0.2.13-0ubuntu1.2)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reproducing the Issue
&lt;/h3&gt;

&lt;p&gt;After running a range of tests on an Ubuntu Server 26.04 virtual machine, I confirmed that the &lt;code&gt;sudo su&lt;/code&gt; processes stayed alive under the following conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect to the server over SSH.&lt;/li&gt;
&lt;li&gt;Start a root shell with &lt;code&gt;sudo su&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Force the SSH connection to close by closing the local terminal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With a reproducible case, I could start narrowing down the cause.&lt;/p&gt;

&lt;h3&gt;
  
  
  "sudo su" vs. "sudo -i"
&lt;/h3&gt;

&lt;p&gt;While looking into &lt;code&gt;sudo su&lt;/code&gt;, I learned that these days, the recommendation is to use &lt;code&gt;sudo -i&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;I repeated the same test with &lt;code&gt;sudo -i&lt;/code&gt;, and no &lt;code&gt;sudo&lt;/code&gt; processes remained after the SSH connection closed. That suggested the &lt;code&gt;su&lt;/code&gt; part of &lt;code&gt;sudo su&lt;/code&gt; was involved.&lt;/p&gt;

&lt;p&gt;With a likely cause in mind, I gave ChatGPT access to the virtual machine and asked it to investigate what was happening.&lt;/p&gt;

&lt;p&gt;Here's a rough summary of what it found. This is a simplified explanation, not a technically precise one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When an SSH connection closes, processes such as the shell launched by SSH are notified of the disconnect through SIGHUP. Whether a parent process forwards SIGHUP to its children depends on the implementation, but programs such as &lt;code&gt;sudo&lt;/code&gt; and &lt;code&gt;bash&lt;/code&gt; do forward it.&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;sudo su&lt;/code&gt;, &lt;code&gt;su&lt;/code&gt; blocks SIGHUP. As a result, the &lt;code&gt;sudo su&lt;/code&gt; processes do not exit unless their child process exits on its own (i.e., the &lt;code&gt;bash&lt;/code&gt; process running under &lt;code&gt;sudo su&lt;/code&gt; stays alive).&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;sudo -i&lt;/code&gt;, the shell launched by &lt;code&gt;sudo&lt;/code&gt; sends SIGHUP to its child process. Once that process exits, &lt;code&gt;sudo -i&lt;/code&gt; exits naturally as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, it seems that SIGHUP from the SSH disconnect did not terminate &lt;code&gt;sudo su&lt;/code&gt; or the processes beneath it. When its parent process exited, &lt;code&gt;sudo su&lt;/code&gt; was reparented to PID 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  "sudo" vs. "sudo-rs"
&lt;/h3&gt;

&lt;p&gt;But I hadn't seen this happen on Ubuntu 24.04 or earlier...&lt;/p&gt;

&lt;p&gt;Then I remembered that Ubuntu 26.04 uses &lt;code&gt;sudo-rs&lt;/code&gt; as its &lt;code&gt;sudo&lt;/code&gt; implementation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User management — sudo-rs (Ubuntu Server documentation): &lt;a href="https://ubuntu.com/server/docs/how-to/security/user-management/#sudo-rs" rel="noopener noreferrer"&gt;https://ubuntu.com/server/docs/how-to/security/user-management/#sudo-rs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I switched from &lt;code&gt;sudo-rs&lt;/code&gt; back to the traditional &lt;code&gt;sudo&lt;/code&gt; implementation and repeated the test. This time, the &lt;code&gt;sudo su&lt;/code&gt; processes exited properly when the SSH connection closed.&lt;/p&gt;

&lt;p&gt;So it seemed that &lt;code&gt;sudo-rs&lt;/code&gt; handled SIGHUP differently from the traditional &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At this point, I asked ChatGPT to look into the implementations of both the traditional &lt;code&gt;sudo&lt;/code&gt; (the sudo-project version) and &lt;code&gt;sudo-rs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From that investigation, it appeared that the traditional &lt;code&gt;sudo&lt;/code&gt; has its own logic for cleaning up child processes when the TTY disconnects, while the newer &lt;code&gt;sudo-rs&lt;/code&gt; does not currently seem to handle that situation well. I haven't modified either implementation to test this, so I can't be certain.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub sudo-project/sudo exec_pty.c: &lt;a href="https://github.com/sudo-project/sudo/blob/1815241c464259564541cc6306e12f2763691d92/src/exec_pty.c#L335-L360" rel="noopener noreferrer"&gt;https://github.com/sudo-project/sudo/blob/1815241c464259564541cc6306e12f2763691d92/src/exec_pty.c#L335-L360&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub trifectatechfoundation/sudo-rs, issue "Better handling of closed user tty": &lt;a href="https://github.com/trifectatechfoundation/sudo-rs/issues/1379" rel="noopener noreferrer"&gt;https://github.com/trifectatechfoundation/sudo-rs/issues/1379&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;sudo-rs&lt;/code&gt; implementation used in Ubuntu 26.04 handles signals differently from the traditional &lt;code&gt;sudo&lt;/code&gt;. This seems like a good opportunity to move away from &lt;code&gt;sudo su&lt;/code&gt; and use &lt;code&gt;sudo -i&lt;/code&gt; (or &lt;code&gt;sudo -s&lt;/code&gt;) instead.&lt;/p&gt;

&lt;p&gt;You could perhaps call this a bug in &lt;code&gt;sudo-rs&lt;/code&gt;. Still, considering that the traditional &lt;code&gt;sudo&lt;/code&gt; has extra logic to work around the issue with &lt;code&gt;sudo su&lt;/code&gt;, avoiding &lt;code&gt;sudo su&lt;/code&gt; may be the safer choice.&lt;/p&gt;

&lt;p&gt;That said, seeing &lt;code&gt;sudo su&lt;/code&gt; processes linger under PID 1 is still pretty unsettling...&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;On Ubuntu 26.04, you can switch between &lt;code&gt;sudo&lt;/code&gt; implementations with the following command:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;update-alternatives &lt;span class="nt"&gt;--config&lt;/span&gt; &lt;span class="nb"&gt;sudo

&lt;/span&gt;There are 2 choices &lt;span class="k"&gt;for &lt;/span&gt;the alternative &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;providing /usr/bin/sudo&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

  Selection    Path                     Priority   Status
&lt;span class="nt"&gt;------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; 0            /usr/lib/cargo/bin/sudo   50        auto mode
  1            /usr/bin/sudo.ws          40        manual mode
  2            /usr/lib/cargo/bin/sudo   50        manual mode

Press &amp;lt;enter&amp;gt; to keep the current choice[&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;, or &lt;span class="nb"&gt;type &lt;/span&gt;selection number:
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can also run either &lt;code&gt;sudo&lt;/code&gt; implementation using its absolute path shown above, without changing the system-wide selection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As a related aside, I learned that &lt;code&gt;nohup&lt;/code&gt; deliberately ignores SIGHUP (&lt;code&gt;SIG_IGN&lt;/code&gt;), rather than blocking it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;User management — sudo-rs (Ubuntu Server documentation): &lt;a href="https://ubuntu.com/server/docs/how-to/security/user-management/#sudo-rs" rel="noopener noreferrer"&gt;https://ubuntu.com/server/docs/how-to/security/user-management/#sudo-rs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;sudo-project/sudo (GitHub): &lt;a href="https://github.com/sudo-project/sudo" rel="noopener noreferrer"&gt;https://github.com/sudo-project/sudo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;trifectatechfoundation/sudo-rs (GitHub): &lt;a href="https://github.com/trifectatechfoundation/sudo-rs" rel="noopener noreferrer"&gt;https://github.com/trifectatechfoundation/sudo-rs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Change History
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;2026/09/22: First version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Personal Blog: &lt;a href="https://mkt-sidenotes.dev/ja/tech/2026-09-20-do-not-use-sudo-su-ubuntu-26/" rel="noopener noreferrer"&gt;https://mkt-sidenotes.dev/ja/tech/2026-09-20-do-not-use-sudo-su-ubuntu-26/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>command</category>
      <category>sudo</category>
      <category>ubuntu</category>
    </item>
    <item>
      <title>Visualizing Year/Month/Day Progress in Grafana</title>
      <dc:creator>mkt</dc:creator>
      <pubDate>Sun, 30 Nov 2025 09:21:51 +0000</pubDate>
      <link>https://dev.to/mkt/visualizing-yearmonthday-progress-in-grafana-1mf4</link>
      <guid>https://dev.to/mkt/visualizing-yearmonthday-progress-in-grafana-1mf4</guid>
      <description>&lt;p&gt;As of writing this post (2025/11/30), apparently around 90% of this year has already passed.&lt;/p&gt;

&lt;p&gt;

&lt;iframe class="tweet-embed" id="tweet-1993184119292149949-95" src="https://platform.twitter.com/embed/Tweet.html?id=1993184119292149949"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1993184119292149949-95');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1993184119292149949&amp;amp;theme=dark"
  }





&lt;/p&gt;

&lt;p&gt;I use Grafana and Prometheus for system monitoring, and I thought it would be cool to see "How much of this year has passed?" directly on my Grafana dashboard.&lt;/p&gt;

&lt;p&gt;So I built it!&lt;/p&gt;

&lt;p&gt;While I was at it, I decided to show not only the progress of this year but also the progress of this month and today.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Here's what the panel looks like:&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.amazonaws.com%2Fuploads%2Farticles%2Fpfarvz0hff5d6qx4c31b.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.amazonaws.com%2Fuploads%2Farticles%2Fpfarvz0hff5d6qx4c31b.png" alt="Time Progress Panel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After some trial and error, I managed to create this using only Grafana and Prometheus (PromQL), which I already use every day.&lt;/p&gt;

&lt;p&gt;The PromQL queries to calculate each value (for UTC) are as follows:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This Year (UTC)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(
  ((day_of_year() - 1) * (3600 * 24)) + (time() % (3600 * 24))
)
/
(
  (365 + (day_of_year() != bool day_of_year(vector(time() + 3600 * 24 * 365)))) * 3600 * 24
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;This Month (UTC)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(
  ((day_of_month() - 1) * (3600 * 24)) + (time() % (3600 * 24))
)
/
(
  days_in_month() * (3600 * 24)
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Today (UTC)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(time() % (3600 * 24)) / (3600 * 24)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you want to display progress in a timezone other than UTC, simply add the appropriate offset (in seconds) to your queries.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UTC+9 (Japan Standard Time; JST): add &lt;code&gt;3600 * 9&lt;/code&gt; (= 32400) seconds&lt;/li&gt;
&lt;li&gt;UTC+2: add &lt;code&gt;3600 * 2&lt;/code&gt; (= 7200) seconds&lt;/li&gt;
&lt;li&gt;UTC-5: add &lt;code&gt;3600 * -5&lt;/code&gt; (= -18000) seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Replace the offsets in the queries to match your local timezone.&lt;/p&gt;

&lt;p&gt;Below are JST examples:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This Year (JST)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# JST
(
  (day_of_year(vector(time() + 3600 * 9) - 1) * 3600 * 24) + ((time() + 3600 * 9) % (3600 * 24) )
)
/
(
  (365 + (day_of_year(vector(time() + 3600 * 9)) != bool day_of_year(vector(time() + 3600 * 9 + 3600 * 24 * 365)))) * 3600 * 24
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;This Month (JST)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# JST
(
  ((day_of_month(vector(time() + 3600 * 9)) - 1) * (3600 * 24)) + ((time() + 3600 * 9) % (3600 * 24))
)
/
(
  days_in_month(vector(time() + 3600 * 9)) * (3600 * 24)
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Today (JST)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# JST
((time() + 3600 * 9) % (3600 * 24)) / (3600 * 24)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you know a better way to do this, let me know 🙇‍♂️&lt;/p&gt;
&lt;h2&gt;
  
  
  Test Environment
&lt;/h2&gt;

&lt;p&gt;Here's the environment I used for testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu Server 24.04

&lt;ul&gt;
&lt;li&gt;Grafana: 12.3.0&lt;/li&gt;
&lt;li&gt;Prometheus: 2.45.3 (the version available via apt on Ubuntu 24.04)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Note: I used Prometheus 2.x for testing. Things might be simpler with 3.x, but I haven't tried yet.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Calculation Formulas
&lt;/h2&gt;

&lt;p&gt;To visualize how much of today, this month, and this year has passed (which I'll call the "progress"), you need to calculate these values.&lt;/p&gt;

&lt;p&gt;The formulas themselves are pretty straightforward: subtract the unixtime at the start of day/month/year from the current unixtime, then divide by the total seconds in day/month/year.&lt;/p&gt;

&lt;p&gt;So, if you can express PromQL queries that match the following formulas, you're good to go:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Today’s&amp;nbsp;Progress&amp;nbsp;(%)&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;3600&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;24&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Current&amp;nbsp;Unixtime&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Unixtime&amp;nbsp;at&amp;nbsp;Today’s&amp;nbsp;00:00&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;100&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;




&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;This&amp;nbsp;Month’s&amp;nbsp;Progress&amp;nbsp;(%)&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;3600&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;24&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Days&amp;nbsp;in&amp;nbsp;Month&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Current&amp;nbsp;Unixtime&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Unixtime&amp;nbsp;at&amp;nbsp;00:00&amp;nbsp;of&amp;nbsp;the&amp;nbsp;1st&amp;nbsp;Day&amp;nbsp;of&amp;nbsp;Month&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;100&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;This&amp;nbsp;Year’s&amp;nbsp;Progress&amp;nbsp;(%)&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;3600&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;24&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Days&amp;nbsp;in&amp;nbsp;Year&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Current&amp;nbsp;Unixtime&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Unixtime&amp;nbsp;at&amp;nbsp;00:00&amp;nbsp;of&amp;nbsp;Jan&amp;nbsp;1st&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;100&lt;/span&gt;&lt;span class="mspace"&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Note: I didn't account for daylight saving time (DST) here.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  PromQL Queries
&lt;/h2&gt;

&lt;p&gt;Let's implement the formulas above using Prometheus Query Language (PromQL), which provides a number of helpful built-in functions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prometheus docs: &lt;a href="https://prometheus.io/docs/prometheus/latest/querying/functions/" rel="noopener noreferrer"&gt;https://prometheus.io/docs/prometheus/latest/querying/functions/&lt;/a&gt;
(&lt;em&gt;Note: Prometheus 2.x and 3.x differ in some literals and syntax. Be careful!&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing to keep in mind: PromQL's time functions operate in UTC. If you want values in your local timezone, you must add an offset manually.&lt;/p&gt;

&lt;p&gt;The following explanations are a bit log, so if you just want the final queries, feel free to scroll back to the TL;DR section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Today's Progress
&lt;/h3&gt;

&lt;p&gt;First, let's calculate today's progress.&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Today’s&amp;nbsp;Progress&amp;nbsp;(%)&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;3600&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;24&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Current&amp;nbsp;Unixtime&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Unixtime&amp;nbsp;at&amp;nbsp;Today’s&amp;nbsp;00:00&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;100&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;In PromQL, you can get the current unixtime by the &lt;code&gt;time()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;You can also calculate today's 00:00 unixtime using the &lt;code&gt;day_of_year()&lt;/code&gt; function, but you don't need to. The number of seconds since 00:00 can be calculated by taking the remainder of the unixtime divided by 86400 (&lt;code&gt;= 3600 * 24&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;So we can use modulo directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# UTC
(time() % (3600 * 24)) / (3600 * 24)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;GitHub Gist: &lt;a href="https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-1_today-progress-utc-promql" rel="noopener noreferrer"&gt;https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-1_today-progress-utc-promql&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to display the value in your local timezone, add the appropriate offset in seconds. For example, for JST (UTC+9), add &lt;code&gt;3600 * 9&lt;/code&gt; (= 32400) seconds.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# JST
((time() + 3600 * 9) % (3600 * 24)) / (3600 * 24)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;GitHub Gist: &lt;a href="https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-2_today-progress-jst-promql" rel="noopener noreferrer"&gt;https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-2_today-progress-jst-promql&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Grafana can multiply by 100 for percentage formatting, so you can leave the raw ratio as-is.&lt;/p&gt;
&lt;h3&gt;
  
  
  This Month's Progress
&lt;/h3&gt;

&lt;p&gt;Next, this month's progress.&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;This&amp;nbsp;Month’s&amp;nbsp;Progress&amp;nbsp;(%)&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;3600&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;24&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Days&amp;nbsp;in&amp;nbsp;Month&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Current&amp;nbsp;Unixtime&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Unixtime&amp;nbsp;at&amp;nbsp;00:00&amp;nbsp;of&amp;nbsp;the&amp;nbsp;1st&amp;nbsp;Day&amp;nbsp;of&amp;nbsp;Month&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;100&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;p&gt;Getting the current unixtime is the same as before, but the tricky part is "Unixtime at 00:00 of the 1st Day of Month" and "Days in Month".&lt;/p&gt;

&lt;p&gt;Obviously, the number of days in a month varies, so you can't use modulo like I did for today's progress.&lt;/p&gt;

&lt;p&gt;Fortunately, PromQL provides the &lt;code&gt;days_in_month()&lt;/code&gt; function for the latter.&lt;/p&gt;

&lt;p&gt;Then, how do you get the unixtime for the 1st day of the month?&lt;/p&gt;

&lt;p&gt;You don't need to calculate it directly. PromQL has a &lt;code&gt;day_of_month()&lt;/code&gt; function that returns the current day of the month, so you can write the numerator as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(day_of_month() - 1) * (3600 * 24)  # Seconds for previous days
+ (time() % (3600 * 24))            # Seconds for today
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So, the query for this month's progress looks like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# UTC
(
  ((day_of_month() - 1) * (3600 * 24)) + (time() % (3600 * 24))
)
/
(
  days_in_month() * (3600 * 24)
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;GitHub Gist: &lt;a href="https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-3_this-month-progress-utc-promql" rel="noopener noreferrer"&gt;https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-3_this-month-progress-utc-promql&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For other timezones, you need to make sure &lt;code&gt;day_of_month()&lt;/code&gt; and &lt;code&gt;days_in_month()&lt;/code&gt; are calculated in your local timezone, not UTC. These functions accept a unixtime (as a vector) as an argument, so you can use your offset-adjusted time.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# JST
(
  ((day_of_month(vector(time() + 3600 * 9)) - 1) * (3600 * 24)) + ((time() + 3600 * 9) % (3600 * 24))
)
/
(
  days_in_month(vector(time() + 3600 * 9)) * (3600 * 24)
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;GitHub Gist: &lt;a href="https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-4_this-month-progress-jst-promql" rel="noopener noreferrer"&gt;https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-4_this-month-progress-jst-promql&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a bit tricky, but it works!&lt;/p&gt;
&lt;h3&gt;
  
  
  This Year's Progress
&lt;/h3&gt;

&lt;p&gt;Finally, this year's progress.&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;This&amp;nbsp;Year’s&amp;nbsp;Progress&amp;nbsp;(%)&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;3600&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord"&gt;24&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Days&amp;nbsp;in&amp;nbsp;Year&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Current&amp;nbsp;Unixtime&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Unixtime&amp;nbsp;at&amp;nbsp;00:00&amp;nbsp;of&amp;nbsp;Jan&amp;nbsp;1st&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;100&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;p&gt;Just like with the month, you can use the &lt;code&gt;day_of_year()&lt;/code&gt; function to get the number of seconds since the start of the year:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(day_of_year() - 1) * (3600 * 24) # Seconds for previous days
+ (time() % (3600 * 24))          # Seconds for today
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only missing piece is "Days in Year".&lt;/p&gt;

&lt;p&gt;Years normally have 365 days, but leap years have 366. If you've ever taken a programming class, you've probably written an if-else statement to check for leap years. But PromQL doesn't have if-else clause, and there's no handy &lt;code&gt;days_in_year()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;So, I asked ChatGPT for help, and it suggested the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# PromQL to calculate the number of days in the year
365
+ clamp_max(
  (
    (
      ((floor(vector(time()) / 31557600) + 1970) % 400) == bool 0
    )
    +
    (
      (((floor(vector(time()) / 31557600) + 1970) % 4) == bool 0)
      *
      (((floor(vector(time()) / 31557600) + 1970) % 100) != bool 0)
    )
  ),
  1
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;31557600 represents 365.25 days * 86400 seconds, an approximation for a year. By dividing the unixtime by this value, you can get the year difference since 1970.&lt;/p&gt;

&lt;p&gt;Also, you can use the &lt;code&gt;bool&lt;/code&gt; modifier to simulate conditional logic. Comparisons return true/false as 1/0, so you can mimic simple if-else behavior.&lt;/p&gt;

&lt;p&gt;Interesting... but since it's an approximation, there might be some error (I haven't checked how much it is).&lt;/p&gt;

&lt;p&gt;After thinking a bit, I realized we can use PromQL's &lt;code&gt;day_of_year()&lt;/code&gt; function to check for leap years. Specifically, if it is a leap year, the length of the year is 366 days, so if you calculate &lt;code&gt;day_of_year()&lt;/code&gt; at the same time 365 days (
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;3600&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;24&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;365&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 seconds) later, the value must be different from the current value.&lt;/p&gt;

&lt;p&gt;So, you can calculate the number of days in the year like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# PromQL to calculate if this year has 365 or 366 days
365
+
(
  day_of_year() != bool day_of_year(vector(time() + (3600 * 24 * 365)))   # Returns 1 or 0
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final query for this year's progress is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# UTC
(
  ((day_of_year() - 1) * (3600 * 24)) + (time() % (3600 * 24))
)
/
(
  (365 + (day_of_year() != bool day_of_year(vector(time() + 3600 * 24 * 365)))) * 3600 * 24
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;GitHub Gist: &lt;a href="https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-5_this-year-progress-utc-promql" rel="noopener noreferrer"&gt;https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-5_this-year-progress-utc-promql&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a bit hard to read, but the same logic applies for any timezone. Just adjust the offset in the query to match your local timezone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# JST
(
  (day_of_year(vector(time() + 3600 * 9) - 1) * 3600 * 24) + ((time() + 3600 * 9) % (3600 * 24) )
)
/
(
  (365 + (day_of_year(vector(time() + 3600 * 9)) != bool day_of_year(vector(time() + 3600 * 9 + 3600 * 24 * 365)))) * 3600 * 24
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;GitHub Gist: &lt;a href="https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-6_this-year-progress-jst-promql" rel="noopener noreferrer"&gt;https://gist.github.com/md-irohas/e8289be61c554d0b59fdd36fa085f5fe#file-6_this-year-progress-jst-promql&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this, you have all the PromQL queries you need for visualization.&lt;/p&gt;

&lt;p&gt;I haven't thoroughly tested all edge cases, so if you spot a bug, please let me know!&lt;/p&gt;

&lt;h2&gt;
  
  
  Visualizing in Grafana
&lt;/h2&gt;

&lt;p&gt;Once the queries are ready, all that's left is to create your favorite panel in Grafana.&lt;/p&gt;

&lt;p&gt;To make a panel like the screenshot above, follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a new panel to your dashboard.&lt;/li&gt;
&lt;li&gt;Select "Bar gauge" as the visualization type.&lt;/li&gt;
&lt;li&gt;Choose Prometheus as the data source (assuming you've already added it).&lt;/li&gt;
&lt;li&gt;Paste the queries for year, month, and day progress. Set the legend to "This Year", "This Month", and "Today" respectively.&lt;/li&gt;
&lt;li&gt;In the right sidebar, configure:

&lt;ul&gt;
&lt;li&gt;Bar gauge:

&lt;ul&gt;
&lt;li&gt;Orientation: Horizontal&lt;/li&gt;
&lt;li&gt;Display mode: Retro LCD&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Standard options:

&lt;ul&gt;
&lt;li&gt;Unit: Misc / Percent (0.0-1.0)&lt;/li&gt;
&lt;li&gt;Min: 0&lt;/li&gt;
&lt;li&gt;Max: 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This should give you a panel that looks like this:&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.amazonaws.com%2Fuploads%2Farticles%2Fpfarvz0hff5d6qx4c31b.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.amazonaws.com%2Fuploads%2Farticles%2Fpfarvz0hff5d6qx4c31b.png" alt="Time Progress Panel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What About Other Datasources?
&lt;/h2&gt;

&lt;p&gt;You can probably do something similar with other datasources, but there are too many to test.&lt;/p&gt;

&lt;p&gt;If you use NodeExporter or Pushgateway, you could create your own custom metrics. PostgreSQL can handle pretty complex SQL, so you could probably calculate it there too (though setting up PostgreSQL just for this might be overkill).&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This article walks through how to visualize the progress of year, month, and day using Grafana + Prometheus.&lt;/p&gt;

&lt;p&gt;Writing these queries in a DSL like PromQL was a fun little puzzle.&lt;/p&gt;

&lt;p&gt;If you try this setup or come up with a cleaner approach, I'd love to hear about it!&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prometheus: &lt;a href="https://prometheus.io/docs/prometheus/latest/querying/functions/" rel="noopener noreferrer"&gt;https://prometheus.io/docs/prometheus/latest/querying/functions/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;2025/11/30: First version.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>grafana</category>
      <category>prometheus</category>
    </item>
    <item>
      <title>Understanding Timestamp Difference between shutdown and systemctl poweroff</title>
      <dc:creator>mkt</dc:creator>
      <pubDate>Wed, 22 Oct 2025 09:48:21 +0000</pubDate>
      <link>https://dev.to/mkt/understanding-timestamp-difference-between-shutdown-and-systemctl-poweroff-4a5l</link>
      <guid>https://dev.to/mkt/understanding-timestamp-difference-between-shutdown-and-systemctl-poweroff-4a5l</guid>
      <description>&lt;p&gt;&lt;em&gt;(This is the English translation of my original Japanese post at &lt;a href="https://zenn.dev/md_irohas/articles/343351fac096e3" rel="noopener noreferrer"&gt;Zenn&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I ran into trouble because I didn't properly understand the difference in time formats between the Linux &lt;code&gt;shutdown&lt;/code&gt; command and the &lt;code&gt;systemctl poweroff&lt;/code&gt; family (same applies to &lt;code&gt;systemctl halt&lt;/code&gt; and &lt;code&gt;systemctl reboot&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;As a reminder to myself, here's a concise summary of each time format and how they differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;shutdown -P 06:00&lt;/code&gt;
-&amp;gt; (On Ubuntu,) this means the next 6:00. If it's before 6:00 now, that's today at 6:00; if it's already past 6:00, it's tomorrow at 6:00.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemctl poweroff --when=06:00&lt;/code&gt;
-&amp;gt; Today at 6:00; if it's already past that time, it executes immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More examples are in the latter half.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu Server 24.04 (systemd 255)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My mistake
&lt;/h2&gt;

&lt;p&gt;When managing servers, you'll sometimes want to schedule a shutdown for planned power outages, etc.&lt;/p&gt;

&lt;p&gt;The other day, when I tried to run the &lt;code&gt;shutdown&lt;/code&gt; command on Ubuntu 24.04, I saw this warning:&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="nv"&gt;$ &lt;/span&gt;shutdown &lt;span class="nt"&gt;--help&lt;/span&gt;
... omitted ...

This is a compatibility interface, please use the more powerful &lt;span class="s1"&gt;'systemctl reboot'&lt;/span&gt;,
&lt;span class="s1"&gt;'systemctl poweroff'&lt;/span&gt;, &lt;span class="s1"&gt;'systemctl reboot'&lt;/span&gt; commands instead.

&lt;span class="c"&gt;# 'systemctl reboot' appears twice... 🤔&lt;/span&gt;
&lt;span class="c"&gt;# It's fixed in the latest systemd source.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ah, so even &lt;code&gt;shutdown&lt;/code&gt; is unified by systemd.&lt;/p&gt;

&lt;p&gt;So I checked systemctl's man page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;poweroff
    ... omitted ...
    This command honors --force and --when= in a similar way as halt.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I can use the &lt;code&gt;--when&lt;/code&gt; option for scheduling.&lt;/p&gt;

&lt;p&gt;Looking at the &lt;code&gt;--when&lt;/code&gt; option description:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--when=
    When used with halt, poweroff, reboot, or kexec, schedule the action
    to be performed at the given timestamp, which should adhere to the
    syntax documented in systemd.time(7) section "PARSING TIMESTAMPS".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't feel like reading all of that, so I quickly asked Copilot.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;(The original chat is in Japanese)&lt;/p&gt;

&lt;p&gt;Me:&lt;br&gt;
Tell me how to write --when option of systemctl poweroff&lt;/p&gt;

&lt;p&gt;Copilot:&lt;br&gt;
The --when option in the systemctl poweroff command is used to specify when to power off the system.&lt;/p&gt;

&lt;p&gt;Basic syntax&lt;br&gt;
sudo systemctl poweroff --when=&lt;time&gt;&lt;/time&gt;&lt;/p&gt;

&lt;p&gt;Common options&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;now: Power off immediately&lt;/li&gt;
&lt;li&gt;reboot: Power off on the next reboot&lt;/li&gt;
&lt;li&gt;shutdown: Power off on the next system shutdown&lt;/li&gt;
&lt;li&gt;Specific time (e.g. 12:30): Power off at the specified time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples&lt;br&gt;
sudo systemctl poweroff --when=now&lt;br&gt;
→ Power off immediately&lt;br&gt;
sudo systemctl poweroff --when=20:00&lt;br&gt;
→ Power off at 20:00 today&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I thought, okay, same as the &lt;code&gt;shutdown&lt;/code&gt; command (THIS WAS THE MISTAKE. Btw, what is reboot or shutdown at a specified time?).&lt;br&gt;
Indeed, the actual &lt;code&gt;shutdown&lt;/code&gt; command is a symbolic link to &lt;code&gt;systemctl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So I assumed that -- just like &lt;code&gt;shutdown&lt;/code&gt; -- if I specify the time as &lt;code&gt;06:00&lt;/code&gt;, it would schedule it for 06:00 the next day, and I ran:&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="nv"&gt;$ &lt;/span&gt;systemctl poweroff &lt;span class="nt"&gt;--when&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"06:00"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit Enter, then...&lt;/p&gt;

&lt;p&gt;It powered off immediately. 😇&lt;/p&gt;

&lt;p&gt;It was just a personal desktop I use casually, so no real harm was done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong?
&lt;/h2&gt;

&lt;p&gt;Let's read the &lt;code&gt;systemd.time&lt;/code&gt; man page section "PARSING TIMESTAMPS."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;... omitted ...

Examples &lt;span class="k"&gt;for &lt;/span&gt;valid timestamps and their normalized form &lt;span class="o"&gt;(&lt;/span&gt;assuming the current &lt;span class="nb"&gt;time &lt;/span&gt;was 2012-11-23 18:15:22 and the timezone was UTC+8, &lt;span class="k"&gt;for &lt;/span&gt;example &lt;span class="s2"&gt;"TZ=:Asia/Shanghai"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;:

      Fri 2012-11-23 11:12:13 -&amp;gt; Fri 2012-11-23 11:12:13
      2012-11-23 11:12:13 -&amp;gt; Fri 2012-11-23 11:12:13
      2012-11-23 11:12:13 UTC -&amp;gt; Fri 2012-11-23 19:12:13
      2012-11-23T11:12:13Z -&amp;gt; Fri 2012-11-23 19:12:13
      2012-11-23T11:12+02:00 -&amp;gt; Fri 2012-11-23 17:12:00
      2012-11-23 -&amp;gt; Fri 2012-11-23 00:00:00
      12-11-23 -&amp;gt; Fri 2012-11-23 00:00:00
      11:12:13 -&amp;gt; Fri 2012-11-23 11:12:13
      11:12 -&amp;gt; Fri 2012-11-23 11:12:00    &amp;lt;- here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you specify only a time, the date is filled in as "today."&lt;br&gt;
If that time has already passed, the action happens immediately.&lt;/p&gt;

&lt;p&gt;A bit unfriendly.&lt;/p&gt;
&lt;h2&gt;
  
  
  How shutdown and systemctl poweroff handle times
&lt;/h2&gt;
&lt;h3&gt;
  
  
  shutdown
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shutdown &lt;span class="o"&gt;[&lt;/span&gt;OPTIONS...] &lt;span class="o"&gt;[&lt;/span&gt;TIME] &lt;span class="o"&gt;[&lt;/span&gt;WALL...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;TIME can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;now&lt;/code&gt;: immediately
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;+n&lt;/code&gt;: n minutes from now
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hh:mm&lt;/code&gt;: the next occurrence of hh:mm (24-hour format). For example, 06:30 means the next 06:30 (this works on Ubuntu; other distros may differ).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also note that the &lt;code&gt;shutdown&lt;/code&gt; command can't specify a date.&lt;/p&gt;

&lt;p&gt;Examples:&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;# If you specify nothing, it's 1 minute later&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;shutdown &lt;span class="nt"&gt;-P&lt;/span&gt;

&lt;span class="c"&gt;# Immediate&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;shutdown &lt;span class="nt"&gt;-P&lt;/span&gt; now

&lt;span class="c"&gt;# The next 06:30 (uses the server's time zone)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;shutdown &lt;span class="nt"&gt;-P&lt;/span&gt; 06:30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  systemctl poweroff
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;--when&lt;/code&gt; option to specify the time.&lt;/p&gt;

&lt;p&gt;The time format can include not only time (&lt;code&gt;hh:mm&lt;/code&gt;) but also a full datetime and timezone.&lt;/p&gt;

&lt;p&gt;If you specify only a time, the date is completed as "today."&lt;br&gt;
If that time has already passed, it executes immediately.&lt;/p&gt;

&lt;p&gt;Examples:&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;# If you specify nothing, it's immediate (differs from shutdown)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl poweroff

&lt;span class="c"&gt;# Run at 06:30 today; if already past, run immediately&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl poweroff &lt;span class="nt"&gt;--when&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"06:30"&lt;/span&gt;

&lt;span class="c"&gt;# Run at 06:30 on 2025-10-05&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl poweroff &lt;span class="nt"&gt;--when&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2025-10-05T06:30:00"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To be safe, avoid time-only strings; specify a full date and time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Read the official documentation properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu 22.04 doesn't have the &lt;code&gt;--when&lt;/code&gt; option yet.
&lt;/li&gt;
&lt;li&gt;You can check how timestamps are parsed with &lt;code&gt;systemd-analyze timestamp "06:30"&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;On Ubuntu (and possibly elsewhere), &lt;code&gt;shutdown&lt;/code&gt; is a symlink to &lt;code&gt;systemctl&lt;/code&gt;, but their time formats are not compatible; &lt;code&gt;shutdown&lt;/code&gt; is implemented as a compatibility command inside &lt;code&gt;systemctl&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Change history
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;2025-10-21: Translated from my original Japanese post.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>command</category>
      <category>mistake</category>
    </item>
  </channel>
</rss>
