<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Abhay V Ashokan — Blog</title>
    <link>https://abhay.app/blog</link>
    <description>Blogs by Abhay V Ashokan</description>
    <language>en</language>
    <atom:link href="https://abhay.app/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Turning my Old MacBook into a Jellyfin Server</title>
      <link>https://abhay.app/blog/turning-my-old-macbook-into-a-jellyfin-server</link>
      <guid isPermaLink="true">https://abhay.app/blog/turning-my-old-macbook-into-a-jellyfin-server</guid>
      <pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<p>Purchasing a Raspberry Pi has been rotting on my wishlist for about a year now. But honestly, if I&#x27;m trying to self-host something like Jellyfin, a Pi alone isn&#x27;t exactly gonna cut it, is it? By the time I&#x27;d buy the Pi, a decent enclosure, and a good SSD, the price tag gets ridiculous. It&#x27;s hard to justify spending that much just to run a &quot;small experiment&quot;.</p><p>Then there&#x27;s my old MacBook. It&#x27;s been sitting in a drawer for about six months now, and the screen has been dead for about ten months. It&#x27;ll cost me a fortune to get it replaced. I could literally purchase a new laptop with that much money. Every once in a while, I hook it up to my monitor to remind myself it still works. Last week, I saw it lying there, and it finally clicked. I could just start here.</p><p>People are out there buying used Mac Minis specifically to build home servers, and a MacBook is basically just a Mac Mini with a battery.</p><a class="header-link" href="#turning-my-macbook-into-a-server"><h2 id="turning-my-macbook-into-a-server">Turning my MacBook into a server</h2><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>While wiping it clean, my first instinct was to install Linux. But trust me, it&#x27;s way easier to stick with macOS.</p><a class="header-link" href="#1-prevent-macbook-from-going-to-sleep"><h3 id="1-prevent-macbook-from-going-to-sleep">1. Prevent MacBook from going to sleep</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>There are a dozen ways out there to prevent your MacBook from sleeping when you close the lid. I plan to keep it running 24x7, so I skipped all third-party apps and went with the nuclear option.</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="bash" data-theme="dark-plus light-plus"><code data-language="bash" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#DCDCAA;--shiki-light:#795E26">sudo</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> pmset</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> -a</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> disablesleep</span><span style="--shiki-dark:#B5CEA8;--shiki-light:#098658"> 1</span></span></code></pre></figure><p>Here, it asks the power management settings to disable the system&#x27;s ability to sleep, irrespective of your other settings.</p><a class="header-link" href="#2-network"><h3 id="2-network">2. Network</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>IP addresses are dynamic by default, but you need a static one to keep your server reachable. You can assign a static IP address by going to <strong>System Settings &gt; Network</strong>. Choose your connection, go to the <strong>TCP/IP</strong> tab in <strong>Details</strong>, change the <strong>Configure IPv4</strong> option to <strong>Using DHCP with manual addresses,</strong> and assign your desired IP address there.</p><a class="header-link" href="#3-ssh-access"><h3 id="3-ssh-access">3. SSH Access</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>Well, this turned out to be surprisingly easier than I thought. Just go to <strong>Settings &gt; General &gt; Sharing</strong> and enable <strong>Remote Login</strong>. Viola, you can now SSH into your MacBook with a password.</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="bash" data-theme="dark-plus light-plus"><code data-language="bash" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#DCDCAA;--shiki-light:#795E26">ssh</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> omega@</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">&lt;</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">ip</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> addres</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">s&gt;</span></span></code></pre></figure><p>SSH keys are more secure than a password. So I switched to that instead. I store mine on 1Password, so I generated one there. You can generate a key pair yourself with this command:</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="bash" data-theme="dark-plus light-plus"><code data-language="bash" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#DCDCAA;--shiki-light:#795E26">ssh-keygen</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> -t</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> ed25519</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> -f</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> ~/.ssh/omega</span></span></code></pre></figure><p>Now, copy the public key into your server using the command:</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="bash" data-theme="dark-plus light-plus"><code data-language="bash" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#DCDCAA;--shiki-light:#795E26">ssh-copy-id</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> -f</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> -i</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> ~/.ssh/omega.pub</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> omega@</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">&lt;</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">ip</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> addres</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">s&gt;</span></span></code></pre></figure><p>Now, disable password-based login entirely. Edit the <code>/etc/ssh/sshd_config</code> with the following setting.</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="rb" data-theme="dark-plus light-plus"><code data-language="rb" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#4FC1FF;--shiki-light:#0070C1">PasswordAuthentication</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> no</span></span>
<span data-line=""><span style="--shiki-dark:#4FC1FF;--shiki-light:#0070C1">PermitEmptyPasswords</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> no</span></span>
<span data-line=""><span style="--shiki-dark:#4FC1FF;--shiki-light:#0070C1">ChallengeResponseAuthentication</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> no</span></span>
<span data-line=""><span style="--shiki-dark:#4FC1FF;--shiki-light:#0070C1">UsePAM</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> no</span></span>
<span data-line=""><span style="--shiki-dark:#4FC1FF;--shiki-light:#0070C1">KbdInteractiveAuthentication</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> no</span></span></code></pre></figure><p>This essentially locks out everyone except those with the key. This section is already getting a bit long, so I won&#x27;t explain what each line does. Please do look it up yourself if you are curious.</p><a class="header-link" href="#installing-jellyfin"><h2 id="installing-jellyfin">Installing Jellyfin</h2><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>By this stage, I already had Docker up and running. My plan was to containerize everything on this machine. But then I hit the Jellyfin documentation and saw this banner right at the top:</p><p><span class="relative md:h-[450px] md:max-h-[450px] h-80 max-h-80 mt-8 mb-12 block"><img alt="Warning for Docker on MacOS" loading="lazy" decoding="async" data-nimg="fill" class="rounded-xl object-contain !w-auto mx-auto border border-muted-foreground" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent" sizes="100vw" srcSet="https://abhay.app/blogs/turning-my-old-macbook-into-a-jellyfin-server/macos-docker-warning.png" src="https://abhay.app/blogs/turning-my-old-macbook-into-a-jellyfin-server/macos-docker-warning.png"/><span class="inline-block text-sm text-muted-foreground opacity-70 absolute text-center w-full -bottom-8 left-1/2 -translate-x-1/2">Warning for Docker on MacOS</span></span></p><p>Basically, it was asking me to just install Jellyfin like any other standard macOS app. I complied. At this point, I felt more like a <a href="https://www.urbandictionary.com/define.php?term=Soydev">Soydev</a> than a hackerman. The only remaining step was to copy my media into the <a href="https://jellyfin.org/docs/general/server/media/shows">Jellyfin-recommended folder structure</a>.</p><p>And just like that, I had my entire library accessible on every device in my home network.</p><p><span class="relative md:h-[450px] md:max-h-[450px] h-80 max-h-80 mt-8 mb-12 block"><img alt="Jellyfin Web and Moonfin mobile app" loading="lazy" decoding="async" data-nimg="fill" class="rounded-xl object-contain !w-auto mx-auto border border-muted-foreground" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent" sizes="100vw" srcSet="https://abhay.app/blogs/turning-my-old-macbook-into-a-jellyfin-server/jellyfin-and-moonfin-ui.png" src="https://abhay.app/blogs/turning-my-old-macbook-into-a-jellyfin-server/jellyfin-and-moonfin-ui.png"/><span class="inline-block text-sm text-muted-foreground opacity-70 absolute text-center w-full -bottom-8 left-1/2 -translate-x-1/2">Jellyfin Web and Moonfin mobile app</span></span></p><p>The media shown in the screenshots is <strong>Caminandes: Llamigos</strong>, an open-source short film produced by the Blender Foundation. It is licensed under <a href="https://creativecommons.org/licenses/by/4.0">Creative Commons Attribution 4.0</a>. You can find more of their work at <a href="https://peach.blender.org">peach.blender.org</a>.</p><a class="header-link" href="#exposing-traffic-to-the-internet"><h2 id="exposing-traffic-to-the-internet">Exposing traffic to the internet</h2><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><a class="header-link" href="#1-tailscale"><h3 id="1-tailscale">1. Tailscale</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>Like any responsible person, I started by installing Tailscale. With a few clicks, I was streaming media on devices outside my network.</p><p>However, I&#x27;m also planning to move <a href="https://abhay.app/blog/how-i-self-hosted-ente-and-how-you-can-too">my Ente photos</a> to this server, and Tailscale&#x27;s private-mesh nature makes it impossible to share album links publicly. So, I scrapped this option.</p><a class="header-link" href="#2-cloudflare-tunnel-and-tailscale-funnel"><h3 id="2-cloudflare-tunnel-and-tailscale-funnel">2. Cloudflare tunnel and Tailscale funnel</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p><a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/">Cloudflare tunnel</a> would have been the path of least resistance for exposing my server to the web for free. Sadly, streaming videos is strictly against their TOS, and I&#x27;m not trying to get my account banned.</p><p>Then there&#x27;s the <a href="https://tailscale.com/docs/features/tailscale-funnel">Tailscale funnel</a>. It&#x27;s a solid free option. It is meant for temporary sharing of local development servers. Reddit users from the Jellyfin community complained about bandwidth issues from time to time. I didn&#x27;t want to abuse a free service either. I dropped this option, too.</p><a class="header-link" href="#3-reverse-proxy-and-port-forwarding"><h3 id="3-reverse-proxy-and-port-forwarding">3. Reverse proxy and port forwarding</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>Probably, the most robust solution is to run a reverse proxy on the server and only allow HTTPS traffic through port 443. As I explained earlier, IP addresses are dynamic by default. Just like your local network, your public IP address changes whenever the IP feels like it. Purchasing a static IP is definitely one option, but the popular workaround is using <a href="https://duckdns.org">Duck DNS</a>.</p><p>Duck DNS acts as your beacon. You run a client on your server that periodically pings the Duck DNS servers with an HTTP request, <em>&quot;Hey, my current IP is <code>&lt;ip addresss&gt;</code>&quot;</em>. To make this work, you have to forward ports 80 and 443 on your router. I went to the login, only to realize the technician had changed the default router password, likely anticipating that I&#x27;d try to mess with it. Now, that was another dead end.</p><a class="header-link" href="#4-building-my-own-tunnel"><h3 id="4-building-my-own-tunnel">4. Building My Own Tunnel</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>This is when I read about creating your own tunnel. I still have my free 1GB EC2 instance on AWS. I could just tunnel my traffic and set up a reverse proxy there. And that&#x27;s exactly what I did.</p><p>I created another SSH key pair, this time to let my home server SSH into the EC2. A simple command opened the tunnel:</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="bash" data-theme="dark-plus light-plus"><code data-language="bash" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#DCDCAA;--shiki-light:#795E26">ssh</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> -f</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> -N</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> -R</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> 8096:localhost:8096</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> admin@</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">&lt;</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">ip</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> addres</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">s&gt;</span></span></code></pre></figure><p>I <a href="https://abhay.app/blog/how-i-self-hosted-ente-and-how-you-can-too#step-3-setting-up-a-reverse-proxy">configured a reverse proxy using Caddy</a> to point to that port. Since Jellyfin runs on port 8096 by default, Caddy just picks up the traffic from the tunnel and serves it over HTTPS. And just like that, I got it working with zero issues.</p><a class="header-link" href="#hacking-my-samsung-tv"><h2 id="hacking-my-samsung-tv">Hacking my Samsung TV</h2><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>There are plenty of third-party clients on both iOS and Android, but my Samsung TV runs on Tizen OS. The official Jellyfin client isn&#x27;t available on the App Store. The workaround was to enable developer mode and push a signed build to the TV over the local network.</p><p>I spent hours trial-and-erroring different methods before finally getting a build to stick. Once I figured it out, I wanted to install a even better-looking Jellyfin client called <a href="https://github.com/Moonfin-Client/Plugin">Moonfin</a>.</p><p>I ended up forking a Tizen installer that worked for me, and built a <a href="https://github.com/AbhayVAshokan/moonfin-tizen-installer">moonfin-tizen-installer</a>. I&#x27;ve published it as a <a href="https://hub.docker.com/r/abhayvashokan/moonfin-installer">Docker image</a> that handles everything from downloading the latest Moonfin release, signing it, and installing it on your Samsung TV with a single command. Convenient.</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="bash" data-theme="dark-plus light-plus"><code data-language="bash" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#DCDCAA;--shiki-light:#795E26">docker</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> run</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> --rm</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> --platform</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> linux/amd64</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> abhayvashokan/moonfin-installer</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> &lt;</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">samsung</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> tv</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> i</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">p&gt;</span></span></code></pre></figure><a class="header-link" href="#what-next"><h2 id="what-next">What next?</h2><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>I&#x27;ve really grown to love the Jellyfin community. There&#x27;s so much room to hack around; plenty of cool integrations, plugins, and custom CSS. I have spent way too many hours customizing the UI to my liking.</p><p>The features are actually impressive. Syncplay stands out; you could start a watch party with your friends, and everyone stays in sync. One thing that&#x27;s missing, and what the community keeps asking for is a native chat feature. I&#x27;m planning to implement that myself, as a plugin ofc.</p><p>It&#x27;s also wild how Jellyfin just handles the heavy lifting. It automatically pulls the movie details, reviews, subtitles, and even cast and crew images. It even transcodes media on the fly for devices that don&#x27;t support a particular format. I&#x27;ve enabled hardware acceleration to use my MacBook&#x27;s GPU via VideoToolbox to reduce load on my CPU. How cool is that?</p><p>Now that I finally have a functional server at home, I&#x27;ll be using it as a playground for a bunch of other experiments.</p>]]></content:encoded>
    </item>
    <item>
      <title>My favorite superhero is Superman!</title>
      <link>https://abhay.app/blog/my-favorite-superhero-is-superman</link>
      <guid isPermaLink="true">https://abhay.app/blog/my-favorite-superhero-is-superman</guid>
      <pubDate>Sun, 15 Feb 2026 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<p>I still remember standing on the edge of my bed with a red pillow cover wrapped around my shoulders like a cape. I would take aim and jump to the table, which stood much taller than the bed, and then make a big leap to the floor, landing with a thump.</p><p>I grew up watching Superman. I loved Superman. Yes, the same Superman who wears a cringy blue costume with bright red underwear. Christopher Reeve&#x27;s Superman. I was mesmerized by watching him fly, fight bad guys, and save the world. I also loved that clumsy Clark Kent, too. I giggled at whoever made fun of him, knowing that <em>he could literally fly circles around them</em>.</p><p>There was this magic in this character that I just couldn&#x27;t let go of. The naive 4-year-old me wanted to be Superman when I grew up. <em>Some dream, huh!</em> Eventually, reality set in. I realized I couldn&#x27;t fly on my own, so I went for the next best thing: becoming a pilot, specifically a fighter pilot. That&#x27;s partly because my dad was in the Air Force, and I grew up watching jets tear through the sky over Bangalore. <em>Ha, that didn&#x27;t work out either, did it?</em></p><p>As I got older, life got in the way. Superman movies went quiet for a while, and suddenly, everyone wanted to be Batman. I hadn&#x27;t seen a single Batman movie till I was sixteen. I mean, <em>who cares about a guy in a bat costume who can&#x27;t even fly?</em></p><p>That&#x27;s when I watched The Dark Knight for the first time. Then I watched it again, and again, and then again. I was obsessed. I had to go back and binge-watch the whole trilogy. I was mindblown to see the depth in each character; it&#x27;s not just a good guy-bad guy superhero movie anymore. There were way too many sharp dialogues and scenes that just hit the right nerve.</p><p>I&#x27;m not a comic book guy. I&#x27;ve only read five in my life, and two were Superman. But lying in bed last night, I realized I still can&#x27;t shake the feeling away. As much as I love Batman, Superman is still the one I look up to. He&#x27;s that pure symbol of hope that brings the four-year-old me right back to the edge of the bed, ready to jump.</p><p><span class="relative md:h-[450px] md:max-h-[450px] h-80 max-h-80 mt-8 mb-12 block"><img alt="Superman &#x27;78" loading="lazy" decoding="async" data-nimg="fill" class="rounded-xl object-contain !w-auto mx-auto border border-muted-foreground" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent" sizes="100vw" srcSet="https://abhay.app/blogs/my-favorite-superhero-is-superman/superman.jpeg" src="https://abhay.app/blogs/my-favorite-superhero-is-superman/superman.jpeg"/><span class="inline-block text-sm text-muted-foreground opacity-70 absolute text-center w-full -bottom-8 left-1/2 -translate-x-1/2">Superman &#x27;78</span></span></p>]]></content:encoded>
    </item>
    <item>
      <title>How I Self-Hosted Ente - And How You Can Too</title>
      <link>https://abhay.app/blog/how-i-self-hosted-ente-and-how-you-can-too</link>
      <guid isPermaLink="true">https://abhay.app/blog/how-i-self-hosted-ente-and-how-you-can-too</guid>
      <pubDate>Sat, 24 May 2025 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<p>Like every other January 1st, I started the year with a little extra motivation to spare. This time, I thought I&#x27;d turn my old laptop into a 24/7 server and host all my photos locally. What I didn&#x27;t realize was… the laptop had already passed away sometime in 2024. I put my old man to rest. I&#x27;d already replaced the batteries three times, and it still wouldn&#x27;t work without being plugged in. Not to mention how slow it has become over the past seven years. I dropped the idea and decided that this year&#x27;s New Year&#x27;s resolution is hitting the gym... again!</p><p>Fast-forward to May, and I pretty much had no choice but to export everything from Google Photos. Gmail had started whining that it might stop sending emails soon. I&#x27;m not about to pay Google ₹100+ per month just to hang on to my photos. I have an ick for subscription plans. In fact, I don&#x27;t even have a YouTube premium subscription.</p><p><a href="https://ente.io/">Ente</a> was an easy choice for me. It runs as a lightweight Go binary with minimal resources. <a href="https://ente.io/blog/ente-on-raspberry-pi/">The lightweight server can be run on a Raspberry PI</a>. Due to its client-side encryption, all heavy lifting including face recognition and semantic search is done on your own device. It&#x27;s wild that they pulled off on-device machine learning with limited processing power and still got it working across different platforms. <a href="https://ente.io/ml">They&#x27;ve written a great breakdown of how they pulled it off</a>.</p><p>Ente&#x27;s founder, Vishnu, a Malayali, was creeped out by seeing how much information Google could infer from photos. So, he left Google to build something different: a reliable, privacy-focused alternative to Google Photos - Ente. In Malayalam, &quot;ente&quot; means &quot;mine,&quot; so &quot;Ente photos&quot; literally translates to &quot;My photos.&quot;</p><p>I created my second AWS account. Well, only God and I know what happened to my first one. Since I don&#x27;t own a credit card, my options were limited. I don&#x27;t plan to use AWS beyond the free tier either. I&#x27;ve seen enough AWS memes to know I&#x27;m probably going to mess something up… again. Well, that&#x27;s future me&#x27;s problem now.</p><p>After a bit of digging around, I decided to go with <a href="https://www.cloudflare.com/developer-platform/products/r2">Cloudflare R2</a> instead of AWS S3 or EBS for storing my photos. R2 offers a pretty generous 10GB of free storage and charges only about ₹1.20 per month for each additional GB. Plus you get millions of free read and write operations every month. Thanks to Ente&#x27;s client-side caching, my usage stays well below the limits, like, over 100x lower. They do not charge anything for egress either, which is wild. Here&#x27;s how I did it.</p><a class="header-link" href="#the-quick-and-easy-setup"><h2 id="the-quick-and-easy-setup">The Quick and Easy Setup</h2><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><a class="header-link" href="#step-1-set-up-docker-if-you-dont-have-it-already"><h3 id="step-1-set-up-docker-if-you-dont-have-it-already">Step 1: Set up Docker if you don&#x27;t have it already</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>I am running a Debian on my EC2 instance, so I just followed <a href="https://docs.docker.com/engine/install/debian">the installation steps</a> in the official docs to get this part up.</p><a class="header-link" href="#step-2-use-the-quickstart-script"><h3 id="step-2-use-the-quickstart-script">Step 2: Use the quickstart script</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>The Ente team has built a quick start script to <a href="https://ente.io/blog/self-hosting-quickstart">setup and run the Ente server with a single command</a>.</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="bash" data-theme="dark-plus light-plus"><code data-language="bash" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#DCDCAA;--shiki-light:#795E26">sh</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> -c</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> &quot;$(</span><span style="--shiki-dark:#DCDCAA;--shiki-light:#795E26">curl</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> -fsSL</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> https://raw.githubusercontent.com/ente-io/ente/main/server/quickstart.sh)&quot;</span></span></code></pre></figure><p>This curl command sets everything up. It creates a <code>my-ente</code> directory with the necessary configuration files, pulls and runs all the containers, and gets things up and running. To test it out, you can forward port <code>3000</code> from your EC2 instance and access your web server at <code>http://&lt;elastic-ip&gt;:3000</code>.</p><a class="header-link" href="#step-3-setting-up-a-reverse-proxy"><h3 id="step-3-setting-up-a-reverse-proxy">Step 3: Setting up a reverse proxy</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>Ente Auth, by design, requires a secure connection (HTTPS) for its communication. It does not support HTTP connections. So, to get things running, you&#x27;ll need a custom domain.</p><p>I&#x27;ve deployed my Ente server at <a href="https://ente.abhay.app">https://ente.abhay.app</a>. To do something similar, start by adding an <code>A</code> record for your domain and point it to your EC2 instance&#x27;s elastic IP. You&#x27;ll also need to set up a reverse proxy. I used <a href="https://caddyserver.com">Caddy</a> for this. It has a lightweight web server with a clean, easy-to-read config file. It fetches and renews TLS certificates by default.</p><p>Firstly, install Caddy on your instance:</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="bash" data-theme="dark-plus light-plus"><code data-language="bash" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#DCDCAA;--shiki-light:#795E26">sudo</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> apt</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> install</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> caddy</span></span></code></pre></figure><p>This will create a <code>Caddyfile</code> at <code>/etc/caddy</code>. Update your <code>Caddyfile</code> in this format:</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="js" data-theme="dark-plus light-plus"><code data-language="js" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">{</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">    email</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080"> ente</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">@</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">abhay</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">.</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">app</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">ente</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">.</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">abhay</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">.</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">app</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> {</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">    reverse_proxy</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> {</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">      to</span><span style="--shiki-dark:#C8C8C8;--shiki-light:#000000"> http</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span><span style="--shiki-dark:#6A9955;--shiki-light:#008000">//localhost:3000</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">    }</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">ente</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">-</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">api</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">.</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">abhay</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">.</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">app</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> {</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">    reverse_proxy</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> {</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">      to</span><span style="--shiki-dark:#C8C8C8;--shiki-light:#000000"> http</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span><span style="--shiki-dark:#6A9955;--shiki-light:#008000">//localhost:8080</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">    }</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">}</span></span></code></pre></figure><p>Now, execute the <code>caddy reload</code> command and you are done. You have your Ente server running on your custom domain.</p><a class="header-link" href="#step-4-setting-up-cloudflare-r2"><h3 id="step-4-setting-up-cloudflare-r2">Step 4: Setting up Cloudflare R2</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>I have used Cloudflare R2 for my setup. You can use any S3-compatible storage provider. By default, Ente&#x27;s compose file is shipped with <a href="https://min.io">MinIO</a>, which itself is a solid starting point.</p><p>To use R2, create a bucket through the Cloudflare dashboard, and then set the following CORS policy:</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="json" data-theme="dark-plus light-plus"><code data-language="json" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">[</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">  {</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#0451A5">    &quot;AllowedOrigins&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: [</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;*&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">],</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#0451A5">    &quot;AllowedMethods&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: [</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;GET&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">, </span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;HEAD&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">, </span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;POST&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">, </span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;PUT&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">, </span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;DELETE&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">],</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#0451A5">    &quot;AllowedHeaders&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: [</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;*&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">],</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#0451A5">    &quot;ExposeHeaders&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: [</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;Etag&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">],</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#0451A5">    &quot;MaxAgeSeconds&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#B5CEA8;--shiki-light:#098658">3000</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">  }</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">]</span></span></code></pre></figure><p>You don&#x27;t need to make the bucket public. Instead, we&#x27;ll use an API token for secure access. Just head over to the <strong>Manage API Tokens</strong> section in the Cloudflare dashboard and generate an access key and secret. We&#x27;ll plug these into the Ente configuration in the next step.</p><a class="header-link" href="#step-5-setting-up-your-museum"><h3 id="step-5-setting-up-your-museum">Step 5: Setting up your museum</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>I really loved Ente&#x27;s reasoning for naming their server museum: <strong><em>&quot;We named our server museum because, for us and our customers, personal photos are worth more than any other piece of art&quot;</em></strong>. I made a few tweaks to the generated configs to make sure everything works with my configuration. Here&#x27;s what my final config looks like. By default, all encryption keys are generated as random hashes.</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="yaml" data-theme="dark-plus light-plus"><code data-language="yaml" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#6A9955;--shiki-light:#008000"># museum.yaml</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">key</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  encryption</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-encryption-key&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  hash</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-encryption-hash&gt;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">jwt</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  secret</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-jwt-secret&gt;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">db</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  host</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">postgres</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  port</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#B5CEA8;--shiki-light:#098658">5432</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  name</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-db-name&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  user</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-pg-user&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  password</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-pg-password&gt;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">s3</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  are_local_buckets</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF">true</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  b2-eu-cen</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    endpoint</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-bucket-endpoint&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    bucket</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-bucket-name&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    key</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-access-key&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    secret</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-access-secret&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    region</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">auto</span></span></code></pre></figure><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="yaml" data-theme="dark-plus light-plus"><code data-language="yaml" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#6A9955;--shiki-light:#008000"># compose.yaml</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">services</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  museum</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    image</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">ghcr.io/ente-io/server</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    ports</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">      - </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">8080:8080</span><span style="--shiki-dark:#6A9955;--shiki-light:#008000"> # API</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    depends_on</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">      postgres</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">        condition</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">service_healthy</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    volumes</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">      - </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">./museum.yaml:/museum.yaml:ro</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">      - </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">./data:/data:ro</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  web</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    image</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">ghcr.io/ente-io/web</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    ports</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">      - </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">3000:3000</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    environment</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">      ENTE_API_ORIGIN</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">https://ente-api.abhay.app</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  postgres</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    image</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">postgres:15</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    environment</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">      POSTGRES_USER</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-pg-user&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">      POSTGRES_PASSWORD</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-pg-password&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">      POSTGRES_DB</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-db-name&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    healthcheck</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">      test</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">pg_isready -q -d &lt;your-db-name&gt; -U &lt;your-pg-user&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">      start_period</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">40s</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">      start_interval</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">1s</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    volumes</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">      - </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">postgres-data:/var/lib/postgresql/data</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">volumes</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  postgres-data</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span></code></pre></figure><a class="header-link" href="#step-6-setting-up-public-albums"><h3 id="step-6-setting-up-public-albums">Step 6: Setting up public albums</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>To enable public sharing of your albums, you&#x27;ll need to run two instances of the web app. One serves as your regular app, and the other runs the same code but on a different origin. Then, you simply configure the main app to use this second instance to handle public links.</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="yaml" data-theme="dark-plus light-plus"><code data-language="yaml" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#6A9955;--shiki-light:#008000"># Rest of the museum.yaml file</span></span>
<span data-line=""> </span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">apps</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  public-albums</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">https://albums.abhay.app</span></span></code></pre></figure><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="yaml" data-theme="dark-plus light-plus"><code data-language="yaml" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#6A9955;--shiki-light:#008000"># Rest of compose.yaml file.</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">web</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  image</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">ghcr.io/ente-io/web</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  ports</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">    - </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">3000:3000</span></span>
<span data-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  environment</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    ENTE_API_ORIGIN</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">https://ente-api.abhay.app</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    ENTE_ALBUMS_ORIGIN</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">https://albums.abhay.app</span></span>
<span data-line="" data-highlighted-line=""> </span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">albums</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  image</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">ghcr.io/ente-io/web</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  ports</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">    - </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">3002:3002</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  environment</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    ENTE_API_ORIGIN</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">https://ente-api.abhay.app</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    ENTE_ALBUMS_ORIGIN</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">https://albums.abhay.app</span></span>
<span data-line=""><span style="--shiki-dark:#6A9955;--shiki-light:#008000"># Rest of compose.yaml file.</span></span></code></pre></figure><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="js" data-theme="dark-plus light-plus"><code data-language="js" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#6A9955;--shiki-light:#008000">// Rest of the Caddyfile</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">albums</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">.</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">abhay</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">.</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">app</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> {</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">    reverse_proxy</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> {</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">      to</span><span style="--shiki-dark:#C8C8C8;--shiki-light:#000000"> http</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span><span style="--shiki-dark:#6A9955;--shiki-light:#008000">//localhost:3002</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">    }</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">}</span></span></code></pre></figure><a class="header-link" href="#step-7-setting-up-backups"><h3 id="step-7-setting-up-backups">Step 7: Setting up backups</h3><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M9.306 10.206a.745.745 0 0 1 .066.977l-.066.076-2.458 2.458a3.228 3.228 0 0 1-4.691-4.431l.126-.133L4.74 6.694a.745.745 0 0 1 1.12.978l-.067.076-2.458 2.458a1.738 1.738 0 0 0 2.353 2.555l.105-.097 2.458-2.458a.745.745 0 0 1 1.054 0ZM9.83 6.17c.29.291.29.763 0 1.054l-2.48 2.48a.745.745 0 1 1-1.054-1.053l2.48-2.48a.745.745 0 0 1 1.054 0Zm3.888-3.887a3.228 3.228 0 0 1 .126 4.431l-.126.133-2.459 2.459a.745.745 0 0 1-1.12-.978l.067-.076 2.458-2.458a1.738 1.738 0 0 0-2.353-2.555l-.105.097-2.458 2.458a.745.745 0 0 1-1.12-.977l.066-.076 2.459-2.458a3.228 3.228 0 0 1 4.565 0Z" fill="#a1a1aa"></path></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"></path></clipPath></defs></svg></a><p>All your uploaded photos are encrypted by default, which is great for privacy. But it also means the data in your bucket is useless without your server. So, having proper backups is essential. Losing your server shouldn&#x27;t mean losing all your photos.</p><p>You&#x27;ll want to back up all the secrets used by your museum server. For PostgreSQL, you&#x27;ll need to back up the entire data volume. To handle this, I set up a separate Docker container that periodically backs up my database to R2.</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="yaml" data-theme="dark-plus light-plus"><code data-language="yaml" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#6A9955;--shiki-light:#008000"># Rest of the compose.yaml file</span></span>
<span data-line=""> </span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">backup</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  image</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">eeshugerman/postgres-backup-s3:15</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">  environment</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">:</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    SCHEDULE</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;@daily&quot;</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    BACKUP_KEEP_DAYS</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#B5CEA8;--shiki-light:#098658">30</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    S3_REGION</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">auto</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    S3_ENDPOINT</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-bucket-endpoint&gt;</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    S3_ACCESS_KEY_ID</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-access-key&gt;</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    S3_SECRET_ACCESS_KEY</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-access-secret&gt;</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    S3_BUCKET</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-bucket-name&gt;</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    S3_PREFIX</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">backups</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    POSTGRES_HOST</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">postgres</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    POSTGRES_DATABASE</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-db-name&gt;</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    POSTGRES_USER</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-pg-user&gt;</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#569CD6;--shiki-light:#800000">    POSTGRES_PASSWORD</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">: </span><span style="--shiki-dark:#CE9178;--shiki-light:#0000FF">&lt;your-pg-password&gt;</span></span></code></pre></figure><p>Ideally, you should also <a href="https://ente.io/reliability">enable replication in Ente</a> - this gives you primary and secondary hot storage, along with cold storage. It&#x27;s actually pretty easy to set up. That said, I&#x27;m skipping it for now since Cloudflare R2 boasts a whopping <strong>99.999999999%</strong> (yep, eleven nines!) annual durability. Realistically, the only way I&#x27;d lose data at this point is me accidentally doing something dumb, which, let&#x27;s be honest, is always the biggest risk anyway.</p><p>I also updated my museum server to block new registrations and set up the <a href="https://ente.io/blog/ente-cli">Ente CLI</a>. Once everything was ready, I <a href="https://ente.io/articles/exporting-google-photos-with-takeout">exported all my Google Photos with Takeout</a>. Over the past couple of years, I had backed up photos in all sorts of weird places. I lost quite a bit of metadata in the process. To fix that, I wrote a bash script using <code>exiftool</code> to restore the correct timestamps and locations. Those are really the only two pieces of metadata I care about.</p><p>It&#x27;s been two weeks since I migrated everything, and I&#x27;ve been really happy with how it&#x27;s all working. My photos and their thumbnails take up just over 11GB, and I&#x27;m expecting my monthly bill to be under ₹3. Not bad at all.</p>]]></content:encoded>
    </item>
    <item>
      <title>Growing Up in Bangalore&apos;s Air Force Quarters</title>
      <link>https://abhay.app/blog/growing-up-in-bangalores-air-force-quarters</link>
      <guid isPermaLink="true">https://abhay.app/blog/growing-up-in-bangalores-air-force-quarters</guid>
      <pubDate>Sun, 13 Oct 2024 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<p>Life felt like a dream back then, when my biggest worry was whether I had a loose tooth. My sister and I would turn our apartment upside down whenever Mum or Dad tried to pull it out. Some days, the neighbors would come knocking on our doors to ask my parents why their children were always crying. But for us, those little battles over loose teeth were part of growing up.</p><p>Sometimes, Mom used to wake up at 3 AM in the morning so that Dad could take his breakfast with him for his early morning shift. I would get up at 6 AM and still be the last one awake. We had a window right beside our bed. The sunlight would have started to seep into the room, yet the view outside remained a blank canvas. No, it&#x27;s not just because of the dew that has condensed on the glass, it was due to the thick, chilly fog that greeted us every morning.</p><p>Yelahanka, as I remember it, was a peaceful suburb with very little traffic. We lived in the Air Force quarters. Mornings were usually a rush with Mum juggling to get my sister and me, who were five and four at that time, ready for school. We were always late since school started at 7:30 AM. She would hold our hands and run the whole 1 km to school. The fog would have started to clear by now revealing a distant hill bathed in a purple hue. To this day, I don&#x27;t know why it&#x27;s purple. Mom always bathed us in hot or cold water every morning and still wonder why we have a running nose. Sweater was part of the uniform. When I joined KV, we had a navy blue sweater to match the blue-and-white uniform. I still wore it whenever I was sick for as long as I could remember.</p><p><span class="relative md:h-[450px] md:max-h-[450px] h-80 max-h-80 mt-8 mb-12 block"><img alt="Sister and me at the Air Force school" loading="lazy" decoding="async" data-nimg="fill" class="rounded-xl object-contain !w-auto mx-auto border border-muted-foreground" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent" sizes="100vw" srcSet="https://abhay.app/blogs/growing-up-in-bangalores-air-force-quarters/sis-and-me-at-school.jpg" src="https://abhay.app/blogs/growing-up-in-bangalores-air-force-quarters/sis-and-me-at-school.jpg"/><span class="inline-block text-sm text-muted-foreground opacity-70 absolute text-center w-full -bottom-8 left-1/2 -translate-x-1/2">Sister and me at the Air Force school</span></span></p><p>I was obsessed with fighter aircrafts, and there used to be a whole bunch of them there. Every year, the Air Force held an Air Show, displaying our fighter jets in stunning formations. I&#x27;d sneak out of my apartment into the street just to catch these aircrafts making rounds during practice. Back then, I dreamed of being a pilot. I&#x27;d tie pillow covers as a cape and jump from the window to the table.</p><p><span class="relative md:h-[450px] md:max-h-[450px] h-80 max-h-80 mt-8 mb-12 block"><img alt="Mom, sister and me at the Air Force station" loading="lazy" decoding="async" data-nimg="fill" class="rounded-xl object-contain !w-auto mx-auto border border-muted-foreground" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent" sizes="100vw" srcSet="https://abhay.app/blogs/growing-up-in-bangalores-air-force-quarters/mom-sis-and-me-at-air-force-station.jpg" src="https://abhay.app/blogs/growing-up-in-bangalores-air-force-quarters/mom-sis-and-me-at-air-force-station.jpg"/><span class="inline-block text-sm text-muted-foreground opacity-70 absolute text-center w-full -bottom-8 left-1/2 -translate-x-1/2">Mom, sister and me at the Air Force station</span></span></p><p>Phone calls were expensive back then, and I remember Mom making calls home in STD booths. Eventually, we did buy a mobile phone, which felt like a luxury. Every vacation, we traveled back to our hometown by train, with Mom always packing lemon rice for the journey. I used to hate it every time. The joy of seeing those familiar faces always made the long journey home feel worth every mile.</p><p>Dad had a scooter—blue, or maybe green? I can&#x27;t quite recall. The four of us made so many memories on that scooter. I&#x27;d stand in the front while we went wherever we needed to go. It still amazes me that it&#x27;s almost 20 years since we left Bangalore, yet these memories still flash vividly in my mind.</p>]]></content:encoded>
    </item>
    <item>
      <title>It Only Took a Million Tries, But My Portfolio Is Done!</title>
      <link>https://abhay.app/blog/it-only-took-a-million-tries-but-my-portfolio-is-done</link>
      <guid isPermaLink="true">https://abhay.app/blog/it-only-took-a-million-tries-but-my-portfolio-is-done</guid>
      <pubDate>Sun, 08 Sep 2024 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[<p>I&#x27;m pretty sure every developer reading this has <em>that moment</em> where you decide to build a portfolio. In your head, it&#x27;s a sleek, perfect site, complete with a dark/light theme toggle and all sorts of cool animations. Let me guess, you never found the time to actually finish it. Yeah, I didn&#x27;t either, three times tbh... until now. This time, decided to skip the Figma, wireframes, or any sort of a design. I just wanted to get it out of my head. I grabbed a pen, sketched a rough idea, and built it using raw Next.js and TailwindCSS.</p><p>I won&#x27;t bore you with the details of how I tried building my portfolio with a different framework each time, only to lose motivation halfway through. Long story short, I spent more time creating elaborate Figma designs or starting with building a theme switcher than actually building the site. I kept getting distracted and lost motivation when I didn&#x27;t see any real progress.</p><p>I just needed a personal space where I can share my random shower thoughts and the new ideas that frequently pop into my head. It&#x27;ll be a mix of many things - sometimes I&#x27;ll dive into some technical jargon, other times I might us it to vent about whatever&#x27;s on my mind. Back in college, I was pretty active on <a href="https://medium.com">Medium</a>, but it feels a bit formal for me now. I want this blog to be a place where I can write freely without worrying about making it perfect.</p><p>Honestly, Next.js made the whole process way easier than I expected. I don&#x27;t need to manually read markdown files, parse them, and turn them into React components during build. <a href="https://nextjs.org/docs/app/building-your-application/configuring/mdx">Next.js and MDX!!</a> Just create a <code>page.mdx</code> file instead of <code>page.tsx</code> with some minimal configuration; kaboom! your blog is done. I made a little <code>BlogLayout</code> component to handle titles and meta information. Here&#x27;s how a blog post will look like:</p><figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#D4D4D4;--shiki-light:#000000;--shiki-dark-bg:#1E1E1E;--shiki-light-bg:#FFFFFF" tabindex="0" data-language="tsx" data-theme="dark-plus light-plus"><code data-language="tsx" data-theme="dark-plus light-plus" style="display:grid"><span data-line=""><span style="--shiki-dark:#C586C0;--shiki-light:#AF00DB">import</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080"> BlogLayout</span><span style="--shiki-dark:#C586C0;--shiki-light:#AF00DB"> from</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> &quot;_/blog-layout&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C586C0;--shiki-light:#AF00DB">export</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF"> const</span><span style="--shiki-dark:#4FC1FF;--shiki-light:#0070C1"> data</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> = {</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">  title:</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515"> &quot;It Only Took a Million Tries, But My Portfolio Is Done!&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">,</span></span>
<span data-line=""><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">  keywords:</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000"> [</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;Next.js&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">, </span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;MDX&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">, </span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;Portfolio&quot;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">],</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#808080;--shiki-light:#800000">&lt;</span><span style="--shiki-dark:#4EC9B0;--shiki-light:#267F99">BlogLayout</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#E50000"> data</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">=</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF">{</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#001080">data</span><span style="--shiki-dark:#569CD6;--shiki-light:#0000FF">}</span><span style="--shiki-dark:#808080;--shiki-light:#800000">&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">  ## Lorem ipsum dolor</span></span>
<span data-line=""><span style="--shiki-dark:#808080;--shiki-light:#800000">  &lt;</span><span style="--shiki-dark:#569CD6;--shiki-light:#800000">img</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#E50000"> src</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">=</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;&quot;</span><span style="--shiki-dark:#9CDCFE;--shiki-light:#E50000"> alt</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">=</span><span style="--shiki-dark:#CE9178;--shiki-light:#A31515">&quot;lorem ipsum&quot;</span><span style="--shiki-dark:#808080;--shiki-light:#800000">&gt;&lt;/</span><span style="--shiki-dark:#569CD6;--shiki-light:#800000">img</span><span style="--shiki-dark:#808080;--shiki-light:#800000">&gt;</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">  **Lorem ipsum** dolor sit amet, consectetur adipiscing elit, sed do eiusmod</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">  tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">  quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">  consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">  cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non</span></span>
<span data-line=""><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">  proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</span></span>
<span data-line=""><span style="--shiki-dark:#808080;--shiki-light:#800000">&lt;/</span><span style="--shiki-dark:#4EC9B0;--shiki-light:#267F99">BlogLayout</span><span style="--shiki-dark:#808080;--shiki-light:#800000">&gt;</span><span style="--shiki-dark:#D4D4D4;--shiki-light:#000000">;</span></span></code></pre></figure><p>Now, let&#x27;s talk about the dark theme. I must admit, I&#x27;m guilty of always asking if a site has a dark mode. But here? Nope. No theme switcher here. That&#x27;s right—this site only comes dark. Sometimes you just have to embrace the dark side and make life a little simpler!</p><p>I&#x27;m pretty pleased with how things turned out. It&#x27;s simple, it&#x27;s responsive, and it has reusable UI elements. I even managed to dynamically generate OG images for each blog post. Yep, <a href="https://nextjs.org/docs/app/api-reference/functions/image-response">Next.js makes that possible too!</a> Here&#x27;s a peek at what it looks like for this blog.</p><p><span class="relative md:h-[450px] md:max-h-[450px] h-80 max-h-80 mt-8 mb-12 block"><img alt="Dynamically rendered OG image" loading="lazy" decoding="async" data-nimg="fill" class="rounded-xl object-contain !w-auto mx-auto border border-muted-foreground" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent" sizes="100vw" srcSet="https://abhay.app/blogs/it-only-took-a-million-tries-but-my-portfolio-is-done/og-image.png" src="https://abhay.app/blogs/it-only-took-a-million-tries-but-my-portfolio-is-done/og-image.png"/><span class="inline-block text-sm text-muted-foreground opacity-70 absolute text-center w-full -bottom-8 left-1/2 -translate-x-1/2">Dynamically rendered OG image</span></span></p><p>I ran a Lighthouse audit and was delighted to see a near-perfect score. I had to tweak a few minor warnings to hit the perfect score with a confetti for my little victory. Yippee!</p><p><span class="relative md:h-[450px] md:max-h-[450px] h-80 max-h-80 mt-8 mb-12 block"><img alt="100% lighthouse score" loading="lazy" decoding="async" data-nimg="fill" class="rounded-xl object-contain !w-auto mx-auto border border-muted-foreground" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;color:transparent" sizes="100vw" srcSet="https://abhay.app/blogs/it-only-took-a-million-tries-but-my-portfolio-is-done/100-percent-lighthouse-score.gif" src="https://abhay.app/blogs/it-only-took-a-million-tries-but-my-portfolio-is-done/100-percent-lighthouse-score.gif"/><span class="inline-block text-sm text-muted-foreground opacity-70 absolute text-center w-full -bottom-8 left-1/2 -translate-x-1/2">100% lighthouse score</span></span></p><p>So, yeah, there you have it. my portfolio is finally live and I couldn&#x27;t be happier. Sometimes, simple is just better. Check out the repository at <a href="https://github.com/AbhayVAshokan/abhay.app">AbhayVAshokan/abhay.app</a>. I&#x27;ll be posting ramdom stuff here, so stay tuned!</p>]]></content:encoded>
    </item>
  </channel>
</rss>
