<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>rogs</title><link>https://rogs.me/tags/wireguard/</link><description>Recent content in wireguard on rogs</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>© Roger Gonzalez</copyright><lastBuildDate>Thu, 02 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://rogs.me/tags/wireguard/index.xml" rel="self" type="application/rss+xml"/><item><title>OpenCode as a server: AI agents that work while I sleep</title><link>https://rogs.me/2026/04/opencode-as-a-server-ai-agents-that-work-while-i-sleep/</link><pubDate>Thu, 02 Apr 2026 00:00:00 +0000</pubDate><guid>https://rogs.me/2026/04/opencode-as-a-server-ai-agents-that-work-while-i-sleep/</guid><description>&lt;p>My main machine is a beast. Ryzen 9 9950X3D, 64 GB of RAM, RX 9060 XT, three
monitors, the works. It barely ever shuts off. So at some point I started
thinking: &lt;em>why isn&amp;rsquo;t this thing working for me when I&amp;rsquo;m not sitting in front of
it?&lt;/em>&lt;/p>
&lt;p>The answer is now: it does. I&amp;rsquo;m running &lt;a href="https://opencode.ai/">OpenCode&lt;/a> as a persistent server on this
machine, accessible from anywhere through my WireGuard VPN. I can spin up coding
sessions from my MacBook Air, my phone, wherever. And the best part? I have
scheduled jobs that run overnight: adding tests, updating documentation,
enforcing code conventions. I wake up to PRs waiting for my review.&lt;/p>
&lt;p>Here&amp;rsquo;s the full setup.&lt;/p>
&lt;h2 id="the-architecture">The architecture&lt;/h2>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil"> ┌─────────────────────────────────────────────────────────┐
│ roger-beast │
│ (Ryzen 9 9950X3D / 64GB) │
│ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ opencode serve │◀─────│ systemd user service │ │
│ │ :4096 (web UI) │ │ (auto-start/restart) │ │
│ └────────┬─────────┘ └──────────────────────┘ │
│ │ │
│ │ ┌──────────────────────┐ │
│ │ │ opencode-scheduler │ │
│ │ │ (systemd timers) │ │
│ │ │ ┌──────────────────┐ │ │
│ │ │ │ 2am: add tests │ │ │
│ │ │ │ 3am: update docs │ │ │
│ │ │ │ 4am: conventions │ │ │
│ │ │ └──────────────────┘ │ │
│ │ └──────────────────────┘ │
│ │ │
└───────────┼─────────────────────────────────────────────┘
│
┌───────┴──────────────┐
│ Nginx Proxy │
│ Manager │
│(opencode.example.com)│
└───────┬──────────────┘
│
┌─────────┴──────────┐
│ WireGuard VPN │
│ / Local Network │
└─────────┬──────────┘
│
┌────────┴────────┐
│ │
┌──┴───┐ ┌─────┴──────┐
│ 💻 │ │ 📱 │
│ MBA │ │ Phone │
└──────┘ └────────────┘
&lt;/code>&lt;/pre>&lt;p>The idea is simple: OpenCode runs as a systemd user service, Nginx Proxy Manager
gives it a nice domain, and WireGuard makes sure only my devices can reach it.
From any browser on any device, I just go to &lt;code>opencode.example.com&lt;/code> and I&amp;rsquo;m in.&lt;/p>
&lt;h2 id="phase-1-opencode-server-with-systemd">Phase 1: OpenCode server with systemd&lt;/h2>
&lt;p>OpenCode has a &lt;code>serve&lt;/code> command that starts a web UI you can access from a
browser. The trick is making it persistent so it survives reboots and restarts
itself if it crashes.&lt;/p>
&lt;p>First, create a systemd user service. This means it runs as your user, not as
root, which is important because it needs access to your home directory, your
API keys, your OpenCode config, everything.&lt;/p>
&lt;p>Create the file at &lt;code>~/.config/systemd/user/opencode.service&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ini" data-lang="ini">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">Description&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">OpenCode headless server&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">After&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">network.target&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">[Service]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">ExecStart&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">/home/roger/.opencode/bin/opencode serve --hostname 0.0.0.0 --port 4096&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">Restart&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">on-failure&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">RestartSec&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">5&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">[Install]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">WantedBy&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">default.target&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few things to note:&lt;/p>
&lt;ul>
&lt;li>&lt;code>--hostname 0.0.0.0&lt;/code> makes it listen on all interfaces, not just localhost.
This is necessary so that Nginx Proxy Manager (or other devices on your
network) can reach it.&lt;/li>
&lt;li>&lt;code>--port 4096&lt;/code> is arbitrary. Pick whatever you want, just make sure it doesn&amp;rsquo;t
conflict with anything else.&lt;/li>
&lt;li>&lt;code>Restart=on-failure&lt;/code> with &lt;code>RestartSec=5&lt;/code> means if OpenCode crashes, systemd
will bring it back up after 5 seconds. I&amp;rsquo;ve never had it crash, but it&amp;rsquo;s
nice to know it&amp;rsquo;s there.&lt;/li>
&lt;li>&lt;code>WantedBy=default.target&lt;/code> means it starts on login. Since this machine
barely ever restarts, that&amp;rsquo;s basically &amp;ldquo;always on.&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>Enable and start it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>systemctl --user daemon-reload
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>systemctl --user enable opencode.service
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>systemctl --user start opencode.service
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify it&amp;rsquo;s running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>systemctl --user status opencode.service
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see it active and running. If you want the service to keep running
even when you&amp;rsquo;re not logged in (which you probably do, since the whole point is
that it runs when you&amp;rsquo;re away), you need to enable lingering:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo loginctl enable-linger roger
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Replace &lt;code>roger&lt;/code> with your username. This tells systemd to keep your user
services running even after you log out. Without this, systemd kills your user
services when your last session closes, which defeats the entire purpose.&lt;/p>
&lt;p>At this point, you should be able to open &lt;code>http://localhost:4096&lt;/code> on the machine
and see the OpenCode web UI.&lt;/p>
&lt;a class="picture-link" href="https://rogs.me/opencode-server-01.webp">
&lt;figure >&lt;img src="https://rogs.me/opencode-server-01.webp">&lt;figcaption>Sweet, sweet OpenCode&lt;/figcaption>&lt;/figure>
&lt;/a>
&lt;h2 id="phase-2-nginx-proxy-manager-plus-wireguard">Phase 2: Nginx Proxy Manager + WireGuard&lt;/h2>
&lt;p>I use &lt;a href="https://nginxproxymanager.com/">Nginx Proxy Manager&lt;/a> as my reverse proxy. It&amp;rsquo;s a Docker-based GUI for
managing Nginx configs, SSL certificates, and proxy hosts. If you prefer raw
Nginx configs, you can absolutely do that instead, the concept is the same:
point a domain at the OpenCode port.&lt;/p>
&lt;p>In Nginx Proxy Manager, I created a new proxy host:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Domain&lt;/strong>: &lt;code>opencode.example.com&lt;/code>&lt;/li>
&lt;li>&lt;strong>Scheme&lt;/strong>: &lt;code>http&lt;/code>&lt;/li>
&lt;li>&lt;strong>Forward Hostname/IP&lt;/strong>: &lt;code>192.168.x.x&lt;/code> (the local IP of my machine)&lt;/li>
&lt;li>&lt;strong>Forward Port&lt;/strong>: &lt;code>4096&lt;/code>&lt;/li>
&lt;li>&lt;strong>Websockets Support&lt;/strong>: enabled (OpenCode&amp;rsquo;s web UI uses websockets)&lt;/li>
&lt;/ul>
&lt;p>For the access part, I don&amp;rsquo;t need to worry too much about authentication because
the domain is only accessible from two places:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>My local network&lt;/strong>: If I&amp;rsquo;m at home, my devices are already on the same
network as the machine.&lt;/li>
&lt;li>&lt;strong>My WireGuard VPN&lt;/strong>: If I&amp;rsquo;m remote, I connect to my WireGuard VPN first,
which puts me on the same network. My WireGuard setup is the same one I
described in my &lt;a href="https://rogs.me/2026/02/claude-code-from-the-beach-remote-coding-setup/">Claude Code from the beach&lt;/a> post.&lt;/li>
&lt;/ol>
&lt;p>The DNS for &lt;code>opencode.example.com&lt;/code> points to the internal IP of the machine
running Nginx Proxy Manager. This means the domain simply doesn&amp;rsquo;t resolve from
the public internet. You&amp;rsquo;d have to be on my network (or VPN) for it to go
anywhere.&lt;/p>
&lt;h2 id="phase-3-accessing-from-anywhere">Phase 3: Accessing from anywhere&lt;/h2>
&lt;p>This is the satisfying part. Once the server is running and the proxy is
configured, the workflow from any device is:&lt;/p>
&lt;ol>
&lt;li>Connect to WireGuard (if I&amp;rsquo;m not already home)&lt;/li>
&lt;li>Open a browser&lt;/li>
&lt;li>Go to &lt;code>opencode.example.com&lt;/code>&lt;/li>
&lt;li>Done. Full OpenCode web UI, all my agents, all my MCP servers, everything.&lt;/li>
&lt;/ol>
&lt;p>From my MacBook Air at a coffee shop, from my phone on the couch, doesn&amp;rsquo;t
matter. The web UI is the same everywhere. I can start a task on my MacBook,
close the laptop, pick it up on my phone later, and everything is still there
because the server is running on the beast at home.&lt;/p>
&lt;p>This pairs really nicely with my &lt;a href="https://rogs.me/2026/02/claude-code-from-the-beach-remote-coding-setup/">Claude Code from the beach&lt;/a> setup, but it&amp;rsquo;s way
friendlier. That setup uses mosh + tmux + SSH bridges through Termux to get a
terminal on a remote machine. It works great for Claude Code (which is a TUI),
but it&amp;rsquo;s a lot of moving parts: you need Termux, SSH keys on your phone, a jump
box, mosh installed everywhere. If something breaks in the chain, you&amp;rsquo;re
debugging SSH configs from a phone keyboard. I wrote a whole blog post about
that setup and I&amp;rsquo;m proud of it, but let&amp;rsquo;s be real: the fact that I needed
&lt;em>an entire blog post&lt;/em> to explain how to use Claude Code from my phone is kind of
the problem.&lt;/p>
&lt;p>With OpenCode, I just open a browser. That&amp;rsquo;s it. Any browser, on any device. No
Termux, no SSH keys, no jump box, no terminal emulator. My phone&amp;rsquo;s regular
browser works perfectly. My MacBook&amp;rsquo;s browser works perfectly. If I ever get a
tablet, that&amp;rsquo;ll work too. The barrier to entry went from &amp;ldquo;install Termux,
configure SSH, set up mosh, create fish aliases&amp;rdquo; to &amp;ldquo;open Firefox.&amp;rdquo;&lt;/p>
&lt;p>Hey Anthropic, if you&amp;rsquo;re reading this: please give Claude Code a web UI. I love
your tool, I pay $100/month for it, but the fact that OpenCode can do this out
of the box and Claude Code can&amp;rsquo;t is&amp;hellip; not great. I shouldn&amp;rsquo;t need a 600-word
phase-by-phase guide to use my coding agent from my phone. Just saying. 🙃&lt;/p>
&lt;p>I still use the Claude Code + mosh + tmux setup for Claude Code specifically
(since it&amp;rsquo;s terminal-only), but for OpenCode work, the web UI is a massive
quality-of-life upgrade for mobile coding.&lt;/p>
&lt;h2 id="phase-4-the-overnight-crew">Phase 4: The overnight crew&lt;/h2>
&lt;p>This is my favorite part. The server runs 24/7, so why not put it to work while
I sleep?&lt;/p>
&lt;p>I use the &lt;a href="https://github.com/different-ai/opencode-scheduler">opencode-scheduler&lt;/a> plugin, which lets you schedule recurring jobs
using your OS&amp;rsquo;s native scheduler (systemd timers on Linux, launchd on Mac). It&amp;rsquo;s
an OpenCode plugin, so you set it up directly from the OpenCode UI.&lt;/p>
&lt;p>First, add the plugin to your &lt;code>opencode.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;plugin&amp;#34;&lt;/span>: [&lt;span style="color:#e6db74">&amp;#34;opencode-scheduler&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then, from the OpenCode UI, you just tell it what you want in natural language:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil">Schedule a job that runs every weekday at 2am and runs the test-gap-pr-cronjob skill
&lt;/code>&lt;/pre>&lt;p>The plugin takes care of creating the systemd timer and service under
&lt;code>~/.config/systemd/user/&lt;/code>. You can verify it&amp;rsquo;s installed with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>systemctl --user list-timers | grep opencode
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="what-my-overnight-jobs-do">What my overnight jobs do&lt;/h3>
&lt;p>I have three scheduled jobs that run between 1 AM and 6 AM while I&amp;rsquo;m sleeping.
Each one uses a custom OpenCode skill (similar to the planning/execution/review
agents I described on my &lt;a href="https://rogs.me/ai">AI Toolbox&lt;/a> page):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>2 AM - Test gap finder&lt;/strong>: Scans the codebase for untested or under-tested
code, writes the missing tests, and opens a PR.&lt;/li>
&lt;li>&lt;strong>3 AM - Documentation updater&lt;/strong>: Checks for outdated or missing docstrings and
README sections, updates them, and opens a PR.&lt;/li>
&lt;li>&lt;strong>4 AM - Convention enforcer&lt;/strong>: Reviews code for style and convention
violations that linters don&amp;rsquo;t catch (naming patterns, architectural decisions,
etc.), fixes them, and opens a PR.&lt;/li>
&lt;/ul>
&lt;p>Each job uses a custom skill that knows the project&amp;rsquo;s conventions, testing
patterns, and documentation style. The skills are the same kind of custom agents
I build for my regular OpenCode workflow, just triggered on a schedule instead of
manually.&lt;/p>
&lt;h3 id="the-morning-routine">The morning routine&lt;/h3>
&lt;p>When I log in in the morning, I usually have 1-3 PRs waiting for me. Most of
them are good to go with minor tweaks. Some need more work. Either way, the
tedious stuff (writing tests for edge cases, updating docstrings, fixing
inconsistent naming) is already done, and I just need to review it.&lt;/p>
&lt;p>It&amp;rsquo;s like having a junior developer who works the night shift. They&amp;rsquo;re not
perfect, but they&amp;rsquo;re reliable, they don&amp;rsquo;t complain, and they&amp;rsquo;re surprisingly
good at the boring stuff.&lt;/p>
&lt;p>You can check the logs for any job at any time:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># From the OpenCode UI&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Show logs &lt;span style="color:#66d9ef">for&lt;/span> test-gap-pr-cronjob
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Or directly on disk&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cat ~/.config/opencode/logs/test-gap-pr-cronjob.log
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="the-specs">The specs&lt;/h2>
&lt;p>For anyone curious about the machine running all of this:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil">OS: Manjaro Linux 26.0.4
Host: B850M Pro-A WiFi
Kernel: 6.12.77-1-MANJARO
CPU: AMD Ryzen 9 9950X3D (32) @ 5.752GHz
GPU: AMD ATI Radeon RX 9060 XT GAMING OC 16G
Memory: 64 GB DDR5
Network: WiFi 6
Uptime: usually measured in days, not hours
&lt;/code>&lt;/pre>&lt;p>The machine is wildly overpowered for this. OpenCode&amp;rsquo;s server uses barely any
resources when idle, and even during active sessions or scheduled jobs, it
doesn&amp;rsquo;t break a sweat. If you have a less powerful machine that stays on, this
setup will work fine for you too.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>The whole setup took maybe 30 minutes. A systemd service, a proxy host, and a
scheduler plugin. That&amp;rsquo;s it.&lt;/p>
&lt;p>What I love about this is that it extends my &lt;a href="https://rogs.me/ai">AI Toolbox&lt;/a> in a way I didn&amp;rsquo;t
expect. I went from &amp;ldquo;I use OpenCode when I&amp;rsquo;m at my desk&amp;rdquo; to &amp;ldquo;OpenCode is always
running and I can use it from anywhere, and it also does work for me while I
sleep.&amp;rdquo; The scheduled jobs alone have saved me hours of tedious work every week.&lt;/p>
&lt;p>If you have a machine that stays on (even a modest home server or an old laptop),
you can do this. You don&amp;rsquo;t need a Ryzen 9 or 64 GB of RAM. You need a machine
that doesn&amp;rsquo;t turn off, a way to reach it remotely, and the willingness to let AI
handle the boring stuff while you&amp;rsquo;re asleep.&lt;/p>
&lt;p>All my configs are public in my dotfiles: &lt;a href="https://git.rogs.me/rogs/dotfiles">git.rogs.me/rogs/dotfiles&lt;/a>&lt;/p>
&lt;p>If you have questions, &lt;a href="https://rogs.me/contact">hit me up&lt;/a>. And if you set this up and wake up to PRs
you didn&amp;rsquo;t write, let me know. That first morning is a great feeling.&lt;/p>
&lt;p>See you in the next one!&lt;/p></description></item><item><title>How I deploy my projects to a single VPS with Gitea, NGINX and Docker</title><link>https://rogs.me/2026/03/how-i-deploy-my-projects-to-a-single-vps-with-gitea-nginx-and-docker/</link><pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate><guid>https://rogs.me/2026/03/how-i-deploy-my-projects-to-a-single-vps-with-gitea-nginx-and-docker/</guid><description>&lt;p>Hello everyone 👋&lt;/p>
&lt;p>A few weeks ago, the team behind &lt;a href="https://jmail.world/">Jmail&lt;/a> (a Gmail-styled interface for browsing
the publicly released Epstein files) shared that they had &lt;a href="https://x.com/rtwlz/status/2020957597810254052">racked up a &lt;strong>$46,485
bill on Vercel&lt;/strong>&lt;/a> The site had gone viral with ~450 million pageviews, and
Vercel&amp;rsquo;s pricing structure turned that into a five-figure invoice. Vercel&amp;rsquo;s CEO
ended up covering the bill personally, which is nice, but not exactly a
scalable solution 😅&lt;/p>
&lt;p>When I saw that story, my first thought was: &lt;em>this is an efficiency problem&lt;/em>.
Jmail is essentially a search interface on top of mostly static content. &lt;a href="https://news.ycombinator.com/item?id=46963473">An SRE
on Hacker News&lt;/a> mentioned they handle 200x Jmail&amp;rsquo;s request load on just two
Hetzner servers. The whole thing could have been served from a moderately sized
VPS for a fraction of the cost.&lt;/p>
&lt;p>That got me thinking about my own setup. I run &lt;strong>everything&lt;/strong> on a single VPS: my
blog, my side projects, my git server, analytics, a wiki, a forum, a secret
sharing tool, and more. The whole thing is held together by NGINX, Gitea, some
bash scripts, and Docker. No Kubernetes, no Terraform, no CI/CD platform with a
$500/month bill. Just a cheap VPS, some config files, and a deployment flow
that&amp;rsquo;s simple enough that I can fix it from my phone at the beach (I&amp;rsquo;ve &lt;a href="https://rogs.me/2026/02/claude-code-from-the-beach-remote-coding-setup/">written
about that before&lt;/a>).&lt;/p>
&lt;p>I get asked about my deployment setup more often than I expected, so I figured
I&amp;rsquo;d write it all down. Let me walk you through the whole thing.&lt;/p>
&lt;h2 id="the-vps">The VPS&lt;/h2>
&lt;p>I&amp;rsquo;m running a &lt;a href="https://www.hetzner.com/cloud/">Hetzner Cloud&lt;/a> CPX21 in Nuremberg, Germany. Here are the specs:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Spec&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>vCPUs&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RAM&lt;/td>
&lt;td>4 GB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Disk&lt;/td>
&lt;td>80 GB SSD&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>OS&lt;/td>
&lt;td>Ubuntu&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Price&lt;/td>
&lt;td>~€7-8/month&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The CPX21 is one of Hetzner&amp;rsquo;s shared vCPU instances. It&amp;rsquo;s cheap, reliable, and
more than enough for what I need. I&amp;rsquo;m usually sitting at around ~10% CPU and
~2GB RAM, so there&amp;rsquo;s plenty of headroom.&lt;/p>
&lt;p>I set up the VPS manually. No Ansible, no configuration management, just plain
old SSH and installing things by hand. I know, I know, &amp;ldquo;infrastructure as code&amp;rdquo;
and all that. But for a single server that I manage myself, the overhead of
automating the setup isn&amp;rsquo;t worth it. If the server dies, I can set it up again
in a couple of hours and restore from backups.&lt;/p>
&lt;h2 id="what-s-running-on-it">What&amp;rsquo;s running on it&lt;/h2>
&lt;p>Here&amp;rsquo;s everything running on this single VPS:&lt;/p>
&lt;h3 id="bare-metal--directly-on-the-server">Bare metal (directly on the server)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Service&lt;/th>
&lt;th>Purpose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://git.rogs.me">Gitea&lt;/a>&lt;/td>
&lt;td>Self-hosted git server&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NGINX&lt;/td>
&lt;td>Web server / reverse proxy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Certbot&lt;/td>
&lt;td>SSL/TLS certificates&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PHP-FPM&lt;/td>
&lt;td>For WordPress sites&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://wiki.rogs.me">DokuWiki&lt;/a>&lt;/td>
&lt;td>Personal wiki&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>fail2ban&lt;/td>
&lt;td>Brute force protection&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>UFW&lt;/td>
&lt;td>Firewall&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A couple WordPress sites&lt;/td>
&lt;td>Various projects&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="docker">Docker&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Service&lt;/th>
&lt;th>Purpose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://ntfy.sh">ntfy&lt;/a>&lt;/td>
&lt;td>Push notifications&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/smallwat3r/shhh">shhh&lt;/a>&lt;/td>
&lt;td>Secret sharing&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://searx.github.io/searx/">SearXNG&lt;/a>&lt;/td>
&lt;td>Privacy-respecting search engine&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>WireGuard&lt;/td>
&lt;td>VPN&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://forum.yams.media">phpBB&lt;/a>&lt;/td>
&lt;td>YAMS community forum&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://umami.is/">Umami&lt;/a>&lt;/td>
&lt;td>Privacy-respecting analytics&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Gitea Actions runner&lt;/td>
&lt;td>CI/CD runner&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://containrrr.dev/watchtower/">Watchtower&lt;/a>&lt;/td>
&lt;td>Automatic Docker image updates&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="static-sites--hugo-served-by-nginx">Static sites (Hugo, served by NGINX)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Site&lt;/th>
&lt;th>Purpose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://rogs.me">rogs.me&lt;/a>&lt;/td>
&lt;td>This blog!&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://montevideo.restaurant">montevideo.restaurant&lt;/a>&lt;/td>
&lt;td>Restaurant directory&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://yams.media">yams.media&lt;/a>&lt;/td>
&lt;td>YAMS documentation site&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That&amp;rsquo;s a lot of stuff for a 4GB VPS. But static sites are basically free in
terms of resources, and the Docker services are all lightweight. The heaviest
things are probably Gitea and the WordPress sites, and even those barely register.&lt;/p>
&lt;h2 id="the-web-server-nginx">The web server: NGINX&lt;/h2>
&lt;p>Every site and service gets its own NGINX config file in &lt;code>/etc/nginx/conf.d/&lt;/code>.
One file per site, nice and clean. No &lt;code>sites-available&lt;/code> / &lt;code>sites-enabled&lt;/code>
symlink dance.&lt;/p>
&lt;p>Here&amp;rsquo;s what a typical config looks like for one of my Hugo sites:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nginx" data-lang="nginx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">server&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">root&lt;/span> &lt;span style="color:#e6db74">/var/www/rogs.me&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">index&lt;/span> &lt;span style="color:#e6db74">index.html&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">server_name&lt;/span> &lt;span style="color:#e6db74">rogs.me&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">location&lt;/span> &lt;span style="color:#e6db74">/&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">try_files&lt;/span> $uri $uri/ =&lt;span style="color:#ae81ff">404&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">listen&lt;/span> &lt;span style="color:#ae81ff">443&lt;/span> &lt;span style="color:#e6db74">ssl&lt;/span>; &lt;span style="color:#75715e"># managed by Certbot
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">ssl_certificate&lt;/span> &lt;span style="color:#e6db74">/etc/letsencrypt/live/rogs.me/fullchain.pem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ssl_certificate_key&lt;/span> &lt;span style="color:#e6db74">/etc/letsencrypt/live/rogs.me/privkey.pem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">include&lt;/span> &lt;span style="color:#e6db74">/etc/letsencrypt/options-ssl-nginx.conf&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ssl_dhparam&lt;/span> &lt;span style="color:#e6db74">/etc/letsencrypt/ssl-dhparams.pem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">server&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">if&lt;/span> &lt;span style="color:#e6db74">(&lt;/span>$host = &lt;span style="color:#e6db74">rogs.me)&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">return&lt;/span> &lt;span style="color:#ae81ff">301&lt;/span> &lt;span style="color:#e6db74">https://&lt;/span>$host$request_uri;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">server_name&lt;/span> &lt;span style="color:#e6db74">rogs.me&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">listen&lt;/span> &lt;span style="color:#ae81ff">80&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">return&lt;/span> &lt;span style="color:#ae81ff">404&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Nothing fancy. Serve files from &lt;code>/var/www/rogs.me&lt;/code>, redirect HTTP to HTTPS,
done. The SSL bits are all managed by Certbot (more on that later).&lt;/p>
&lt;p>For Docker services, the config looks slightly different because NGINX acts as a
reverse proxy:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nginx" data-lang="nginx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">server&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">server_name&lt;/span> &lt;span style="color:#e6db74">analytics.rogs.me&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">location&lt;/span> &lt;span style="color:#e6db74">/&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_pass&lt;/span> &lt;span style="color:#e6db74">http://localhost:3000&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Host&lt;/span> $host;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Real-IP&lt;/span> $remote_addr;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Forwarded-For&lt;/span> $proxy_add_x_forwarded_for;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Forwarded-Proto&lt;/span> $scheme;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">listen&lt;/span> &lt;span style="color:#ae81ff">443&lt;/span> &lt;span style="color:#e6db74">ssl&lt;/span>; &lt;span style="color:#75715e"># managed by Certbot
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e"># ... SSL config same as above
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Same pattern: one file per service, NGINX handles SSL termination, and proxies
to whatever port the Docker container exposes on localhost.&lt;/p>
&lt;h2 id="ssl-tls-with-let-s-encrypt">SSL/TLS with Let&amp;rsquo;s Encrypt&lt;/h2>
&lt;p>All certificates come from &lt;a href="https://letsencrypt.org/">Let&amp;rsquo;s Encrypt&lt;/a> via Certbot. I installed it with
&lt;code>apt&lt;/code> and used the NGINX plugin:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo apt install certbot python3-certbot-nginx
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo certbot --nginx -d rogs.me
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Certbot modifies the NGINX config automatically to add the SSL directives
(that&amp;rsquo;s why you see those &lt;code># managed by Certbot&lt;/code> comments).&lt;/p>
&lt;p>Certificates auto-renew daily at 3 AM via a cron job:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#ae81ff">3&lt;/span> * * * certbot renew -q
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>-q&lt;/code> flag keeps it quiet: no output unless something goes wrong. Certbot
is smart enough to only renew certificates that are close to expiring, so
running it daily is fine.&lt;/p>
&lt;h2 id="self-hosted-git-with-gitea">Self-hosted git with Gitea&lt;/h2>
&lt;p>I use &lt;a href="https://gitea.com/">Gitea&lt;/a> as my primary git server. It runs bare metal on the VPS (not in
Docker) and lives at &lt;a href="https://git.rogs.me">git.rogs.me&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Why Gitea instead of just using GitHub?&lt;/strong> I want to own my git infrastructure.
GitHub is great for collaboration, but I like having control over where my code
lives. If GitHub goes down or decides to change their terms, my repos are safe
on my own server.&lt;/p>
&lt;p>That said, I mirror everything to both GitHub and GitLab so other people can
collaborate, open issues, and submit PRs. Best of both worlds: I own the
primary, and the mirrors handle the social coding side.&lt;/p>
&lt;h3 id="gitea-actions">Gitea Actions&lt;/h3>
&lt;p>Gitea has a built-in CI/CD system called &lt;a href="https://docs.gitea.com/usage/actions/overview">Gitea Actions&lt;/a> that&amp;rsquo;s compatible with
GitHub Actions workflows. The runner is the official &lt;code>gitea/act_runner&lt;/code> Docker
image, running on the same VPS. Pretty vanilla setup, no custom configuration.&lt;/p>
&lt;p>This is the core of my deployment pipeline. Every time I push to &lt;code>master&lt;/code>, Gitea
Actions picks up the workflow and deploys the site.&lt;/p>
&lt;h2 id="deploying-hugo-sites">Deploying Hugo sites&lt;/h2>
&lt;p>This is where it all comes together. All three of my Hugo sites follow the exact
same deployment pattern. Here&amp;rsquo;s the flow:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil"> ┌──────────┐ push ┌──────────┐ Gitea Actions ┌──────────┐
│ Local │────────────────▶ │ Gitea │ ────────────────────▶│ Runner │
│ machine │ │(git.rogs)│ │ (Docker) │
└──────────┘ └──────────┘ └────┬─────┘
│
SSH into same VPS
│
▼
┌──────────┐
│ VPS │
│ git pull │
│ build.sh │
└────┬─────┘
│
Hugo builds to
/var/www/domain/
│
▼
┌──────────┐
│ NGINX │
│ serves │
└──────────┘
&lt;/code>&lt;/pre>&lt;p>Yes, the Gitea Actions runner SSHes into the same server it&amp;rsquo;s running on. I know
that&amp;rsquo;s a bit redundant, but I designed it this way on purpose: if I ever move my
hosting somewhere else (or switch back to GitHub Actions), the workflow doesn&amp;rsquo;t
need to change. The SSH target is just a secret, so I swap an IP address and
everything keeps working.&lt;/p>
&lt;h3 id="the-gitea-actions-workflow">The Gitea Actions workflow&lt;/h3>
&lt;p>Here&amp;rsquo;s the workflow file that lives in &lt;code>.gitea/workflows/deploy.yml&lt;/code> in each
repo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">deploy&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">on&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">push&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">branches&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">master&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">jobs&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">deploy&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">runs-on&lt;/span>: &lt;span style="color:#ae81ff">ubuntu-latest&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">steps&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">Deploy via SSH&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">uses&lt;/span>: &lt;span style="color:#ae81ff">appleboy/ssh-action@v1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">with&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">host&lt;/span>: &lt;span style="color:#ae81ff">${{ secrets.SSH_HOST }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">username&lt;/span>: &lt;span style="color:#ae81ff">${{ secrets.SSH_USER }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">key&lt;/span>: &lt;span style="color:#ae81ff">${{ secrets.SSH_PRIVATE_KEY }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">port&lt;/span>: &lt;span style="color:#ae81ff">${{ secrets.SSH_PORT }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">script&lt;/span>: |&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> cd repo &amp;amp;&amp;amp; git stash &amp;amp;&amp;amp; git pull --force origin master &amp;amp;&amp;amp; ./build.sh&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It&amp;rsquo;s beautifully simple:&lt;/p>
&lt;ol>
&lt;li>Push to &lt;code>master&lt;/code> triggers the workflow&lt;/li>
&lt;li>The runner uses &lt;a href="https://github.com/appleboy/ssh-action">appleboy/ssh-action&lt;/a> to SSH into the server&lt;/li>
&lt;li>On the server: stash any local changes, pull the latest code, and run the
build script&lt;/li>
&lt;/ol>
&lt;p>The &lt;code>git stash&lt;/code> is there as a safety net. The WebP conversion in the build
script modifies tracked files (more on that in a second), so without the stash,
&lt;code>git pull&lt;/code> would complain about dirty working tree.&lt;/p>
&lt;p>All four secrets (&lt;code>SSH_HOST&lt;/code>, &lt;code>SSH_USER&lt;/code>, &lt;code>SSH_PRIVATE_KEY&lt;/code>, &lt;code>SSH_PORT&lt;/code>) are
configured in Gitea&amp;rsquo;s repository settings. The SSH key has access to the server
but is locked down to only what the deployment needs.&lt;/p>
&lt;h3 id="the-build-script">The build script&lt;/h3>
&lt;p>Every Hugo site has a &lt;code>build.sh&lt;/code> in the repo root. Here&amp;rsquo;s the one for this blog:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Convert all images to WebP for better performance&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> file in &lt;span style="color:#66d9ef">$(&lt;/span>git ls-files --others --cached --exclude-standard &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> | grep -v &lt;span style="color:#e6db74">&amp;#39;.git&amp;#39;&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> | grep -E &lt;span style="color:#e6db74">&amp;#39;\.(png|jpg|jpeg)$&amp;#39;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>; &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cwebp -lossless &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$file&lt;span style="color:#e6db74">&amp;#34;&lt;/span> -o &lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>file%.*&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">.webp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">done&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Update all references from png/jpg/jpeg to webp&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> tracked_file in &lt;span style="color:#66d9ef">$(&lt;/span>git ls-files --others --cached --exclude-standard &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> | grep -v &lt;span style="color:#e6db74">&amp;#39;.git&amp;#39;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>; &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sed -i &lt;span style="color:#e6db74">&amp;#39;s/\.png/.webp/g&amp;#39;&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$tracked_file&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sed -i &lt;span style="color:#e6db74">&amp;#39;s/\.jpg/.webp/g&amp;#39;&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$tracked_file&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sed -i &lt;span style="color:#e6db74">&amp;#39;s/\.jpeg/.webp/g&amp;#39;&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$tracked_file&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">done&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Build the site&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>hugo -s . -d /var/www/rogs.me/ --minify --cacheDir $PWD/hugo-cache
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Three things happen here:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Image optimization&lt;/strong>: Every PNG, JPG, and JPEG gets converted to WebP using
&lt;code>cwebp&lt;/code> (lossless mode, so no quality loss). WebP files are significantly
smaller than their originals.&lt;/li>
&lt;li>&lt;strong>Reference rewriting&lt;/strong>: All file references get updated from &lt;code>.png&lt;/code> /
&lt;code>.jpg&lt;/code> / &lt;code>.jpeg&lt;/code> to &lt;code>.webp&lt;/code>. This is why we need &lt;code>git stash&lt;/code> in the
workflow; this step modifies tracked files.&lt;/li>
&lt;li>&lt;strong>Hugo build&lt;/strong>: Generates the static site with minification enabled and outputs
it directly to &lt;code>/var/www/rogs.me/&lt;/code>. NGINX is already configured to serve from
that directory, so the site is live immediately.&lt;/li>
&lt;/ol>
&lt;p>The &lt;code>--cacheDir&lt;/code> flag keeps Hugo&amp;rsquo;s build cache in the repo directory, which
speeds up subsequent builds.&lt;/p>
&lt;p>Each site&amp;rsquo;s &lt;code>build.sh&lt;/code> is essentially identical, just with a different output
path (&lt;code>montevideo.restaurant&lt;/code>, &lt;code>yams.media&lt;/code>, etc.).&lt;/p>
&lt;h3 id="variations-across-sites">Variations across sites&lt;/h3>
&lt;p>While the pattern is the same, there are small differences:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>yams.media&lt;/strong> has a two-job workflow: a &lt;code>test_build&lt;/code> job runs Hugo in a Docker
container first to make sure the build succeeds, and only then does the deploy
job run. This is because the YAMS docs site has more contributors, so I want
to catch build errors before they hit production.&lt;/li>
&lt;li>&lt;strong>yams.media&lt;/strong> also uses &lt;code>--cleanDestinationDir&lt;/code> and &lt;code>--gc&lt;/code> flags for a cleaner
build output.&lt;/li>
&lt;/ul>
&lt;h2 id="docker-services-and-watchtower">Docker services and Watchtower&lt;/h2>
&lt;p>Most of my non-static services run in Docker with &lt;code>docker-compose&lt;/code>. Each
service has its own directory in &lt;code>/opt/&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil">/opt/
├── analytics.rogs.me/ # Umami
│ └── docker-compose.yml
├── ntfy/
│ └── docker-compose.yml
├── shhh/
│ └── docker-compose.yml
├── searx/
│ └── docker-compose.yml
└── ...
&lt;/code>&lt;/pre>&lt;p>For updates, I use &lt;a href="https://containrrr.dev/watchtower/">Watchtower&lt;/a>. It runs as a Docker container itself and
periodically checks if there are newer images available for my running
containers. If there are, it pulls the new image, stops the old container, and
starts a new one with the same configuration.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">watchtower&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">containrrr/watchtower&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">/var/run/docker.sock:/var/run/docker.sock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">unless-stopped&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Is this a bit risky? Sure. An automatic update could break something. But in
practice, it hasn&amp;rsquo;t failed me once, and the services I&amp;rsquo;m running are stable
enough that breaking changes in Docker images are rare. For a personal setup,
the convenience of never having to manually update containers is worth the small
risk.&lt;/p>
&lt;h2 id="security">Security&lt;/h2>
&lt;p>I&amp;rsquo;m not running a bank here, but I do take basic security seriously:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>UFW (Uncomplicated Firewall)&lt;/strong>: Only NGINX ports (80, 443) and SSH are open.
Everything else is blocked.&lt;/li>
&lt;li>&lt;strong>fail2ban&lt;/strong>: Watches SSH logs and bans IPs after too many failed login
attempts. Essential if your SSH port is exposed to the internet.&lt;/li>
&lt;li>&lt;strong>SSH keys only&lt;/strong>: Password authentication is disabled. If you don&amp;rsquo;t have the
key, you&amp;rsquo;re not getting in.&lt;/li>
&lt;li>&lt;strong>Let&amp;rsquo;s Encrypt everywhere&lt;/strong>: Every site and service gets HTTPS. No exceptions.&lt;/li>
&lt;li>&lt;strong>Docker services on localhost&lt;/strong>: All Docker containers bind to &lt;code>localhost&lt;/code>.
They&amp;rsquo;re only accessible through the NGINX reverse proxy, which handles SSL
termination.&lt;/li>
&lt;/ul>
&lt;!--listend-->
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Quick UFW setup&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw default deny incoming
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw default allow outgoing
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#e6db74">&amp;#39;Nginx Full&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow ssh
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw enable
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="dns">DNS&lt;/h2>
&lt;p>All my domains use &lt;a href="https://www.cloudflare.com/">Cloudflare&lt;/a> for DNS. But &lt;strong>only DNS&lt;/strong> for most of them. I&amp;rsquo;m
not using Cloudflare&amp;rsquo;s CDN or proxy features on my main sites. The DNS records
point directly to my VPS IP with the proxy toggle set to &amp;ldquo;DNS only&amp;rdquo; (the grey
cloud, not the orange one).&lt;/p>
&lt;p>Why Cloudflare for DNS? Two reasons. First, it&amp;rsquo;s free, fast, and the dashboard
is easy to use. Second, and more importantly: if something goes wrong, I can
switch to using Cloudflare&amp;rsquo;s full proxy and DDoS protection &lt;strong>with the flick of a
button&lt;/strong>. Just toggle the grey cloud to orange and you&amp;rsquo;re behind Cloudflare&amp;rsquo;s
network instantly.&lt;/p>
&lt;p>I&amp;rsquo;ve already had to do this once. &lt;a href="https://forum.yams.media">forum.yams.media&lt;/a> (the YAMS community forum)
was getting DDoSed and swarmed by bots constantly. Flipping that toggle to
orange solved the problem immediately. The rest of my sites run without
Cloudflare&amp;rsquo;s proxy because they don&amp;rsquo;t need it, but knowing I can turn it on in
seconds gives me peace of mind.&lt;/p>
&lt;h2 id="backups">Backups&lt;/h2>
&lt;p>This is the part that most people skip. Don&amp;rsquo;t be most people.&lt;/p>
&lt;p>My backup strategy has two stages:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil"> ┌─────────────┐ 11 PM cron ┌───────────────────┐
│ VPS │ ───────────────▶│ /home/backups/ │
│ (services) │ tar + GPG │ (encrypted .gpg) │
└─────────────┘ └─────────┬─────────┘
│
midnight cron
(SSH pull)
│
▼
┌──────────────────┐
│ Home Server │
│ (NAS + S3) │
└──────────────────┘
&lt;/code>&lt;/pre>&lt;h3 id="stage-1-backup-on-the-vps--11-pm">Stage 1: Backup on the VPS (11 PM)&lt;/h3>
&lt;p>Every night at 11 PM, a series of cron jobs run backup scripts for each service.
Each script follows the same pattern:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>BACKUP_DIR&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;/home/backups/servicename&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TARGET_DIR&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;/path/to/service&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>DATE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>date +%Y-%m-%d-%s&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>BACKUP_FILE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>$BACKUP_DIR&lt;span style="color:#e6db74">/backup-servicename-&lt;/span>$DATE&lt;span style="color:#e6db74">.tar.zst&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ENCRYPTED_FILE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>$BACKUP_FILE&lt;span style="color:#e6db74">.gpg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>LOG_FILE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;/var/log/backup_servicename.log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>GPG_RECIPIENT&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;your-email@example.com&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>log_message&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> echo &lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>date +&lt;span style="color:#e6db74">&amp;#39;%Y-%m-%d %H:%M:%S&amp;#39;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>&lt;span style="color:#e6db74"> - &lt;/span>$1&lt;span style="color:#e6db74">&amp;#34;&lt;/span> | tee -a &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$LOG_FILE&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>log_message &lt;span style="color:#e6db74">&amp;#34;=== Starting backup ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>mkdir -p &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$BACKUP_DIR&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># For Docker services: stop containers first&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker compose stop
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Create compressed archive&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>tar -caf &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$BACKUP_FILE&lt;span style="color:#e6db74">&amp;#34;&lt;/span> -C &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$TARGET_DIR&lt;span style="color:#e6db74">&amp;#34;&lt;/span> .
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Encrypt with GPG&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>gpg --encrypt --armor -r &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$GPG_RECIPIENT&lt;span style="color:#e6db74">&amp;#34;&lt;/span> -o &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$ENCRYPTED_FILE&lt;span style="color:#e6db74">&amp;#34;&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$BACKUP_FILE&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>rm -f &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$BACKUP_FILE&lt;span style="color:#e6db74">&amp;#34;&lt;/span> &lt;span style="color:#75715e"># Remove unencrypted version&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># For Docker services: restart containers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker compose up -d
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>log_message &lt;span style="color:#e6db74">&amp;#34;=== Backup completed ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Key points:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Compression&lt;/strong>: I use &lt;code>tar.zst&lt;/code> (&lt;a href="https://facebook.github.io/zstd/">Zstandard&lt;/a>) for compression. It&amp;rsquo;s faster than
gzip and produces smaller files.&lt;/li>
&lt;li>&lt;strong>Encryption&lt;/strong>: Every backup gets GPG-encrypted before it touches the network.
Even if someone gets access to the backup files, they&amp;rsquo;re useless without my
private key.&lt;/li>
&lt;li>&lt;strong>Docker services&lt;/strong>: For services running in Docker, the script stops the
containers before backing up to ensure data consistency, then starts them again.
This causes a brief downtime (usually a few seconds), which is fine for
personal services at 11 PM.&lt;/li>
&lt;li>&lt;strong>Database dumps&lt;/strong>: For services with databases (like Gitea, which uses MySQL),
the script dumps the database separately with &lt;code>mysqldump&lt;/code> before creating the
archive.&lt;/li>
&lt;li>&lt;strong>Logging&lt;/strong>: Every step is logged to &lt;code>/var/log/&lt;/code>, so I can check if something
went wrong.&lt;/li>
&lt;/ul>
&lt;h3 id="stage-2-pull-to-home-server--midnight">Stage 2: Pull to home server (midnight)&lt;/h3>
&lt;p>At midnight, my home server SSHes into the VPS and pulls all the encrypted
backup files to my local NAS. From there, they also get pushed to an S3 bucket.&lt;/p>
&lt;p>This gives me the classic &lt;strong>3-2-1 backup strategy&lt;/strong>: 3 copies of the data (VPS,
NAS, S3), on 2 different media types, with 1 offsite copy. If Hetzner&amp;rsquo;s
datacenter burns down, I have everything locally. If my house burns down, I have
everything in S3.&lt;/p>
&lt;h2 id="monitoring">Monitoring&lt;/h2>
&lt;p>I run &lt;a href="https://github.com/louislam/uptime-kuma">Uptime Kuma&lt;/a> on my home server to monitor all my services. It checks
every site and service periodically and sends me a notification (via ntfy,
naturally) if something goes down.&lt;/p>
&lt;p>It&amp;rsquo;s not fancy, but it works. I&amp;rsquo;ve caught a few issues before anyone else
noticed them, which is the whole point.&lt;/p>
&lt;h2 id="the-big-picture">The big picture&lt;/h2>
&lt;p>Here&amp;rsquo;s what the whole setup looks like:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil"> ┌─────────────────────────────────────────────────────────┐
│ Hetzner CPX21 │
│ │
│ ┌─────────┐ ┌──────────────────────────────────┐ │
│ │ Gitea │ │ NGINX │ │
│ │ Actions │ │ ┌──────────┐ ┌──────────────┐ │ │
│ │ Runner │ │ │ Static │ │ Reverse │ │ │
│ │ (Docker) │ │ │ sites │ │ proxy to │ │ │
│ └────┬─────┘ │ │/var/www/ │ │ Docker svcs │ │ │
│ │ │ └──────────┘ └──────────────┘ │ │
│ │ SSH │ ▲ │ │ │
│ │ └────────┼──────────────┼──────────┘ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌─────────┐ ┌───────┐ ┌───────────┐ │
│ │ Git │──build──│ Hugo │ │ Docker │ │
│ │ repos │ │ sites │ │ services │ │
│ └─────────┘ └───────┘ └───────────┘ │
│ │
│ ┌─────────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Gitea │ │ Certbot │ │ fail2ban │ │
│ │ (bare metal)│ │ (SSL) │ │ + UFW │ │
│ └─────────────┘ └──────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────┘
&lt;/code>&lt;/pre>&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>The whole philosophy here is &lt;strong>simplicity&lt;/strong>. There&amp;rsquo;s no orchestration tool, no
container registry, no deployment platform. It&amp;rsquo;s just:&lt;/p>
&lt;ol>
&lt;li>Push code to Gitea&lt;/li>
&lt;li>A workflow SSHes into the server&lt;/li>
&lt;li>Git pull + bash script builds the site&lt;/li>
&lt;li>NGINX serves it&lt;/li>
&lt;/ol>
&lt;p>Could I make this more sophisticated? Sure. Could I use Ansible to manage the
server config, or Kubernetes to orchestrate the containers, or a proper CI/CD
platform with build artifacts and rollbacks? Absolutely. But for a personal
setup that hosts a blog, some side projects, and a handful of services, this is
more than enough.&lt;/p>
&lt;p>The setup has been running for years with minimal maintenance. The most time I
spend on it is writing backup scripts for new services and adding NGINX configs
when I deploy something new. Everything else is automated: deployments, SSL
renewals, Docker updates, backups.&lt;/p>
&lt;p>If you&amp;rsquo;re thinking about self-hosting your projects, my advice is: &lt;strong>start
simple&lt;/strong>. A VPS, NGINX, and a bash script can take you surprisingly far. You
can always add complexity later if you need it, but in my experience, you
probably won&amp;rsquo;t.&lt;/p>
&lt;p>If you have questions about any part of this setup, feel free to reach out on
the &lt;a href="https://rogs.me/contact">Contact&lt;/a> page. I&amp;rsquo;m always happy to help people get started with self-hosting.&lt;/p>
&lt;p>See you in the next one!&lt;/p></description></item><item><title>Adding analytics to my blog</title><link>https://rogs.me/2026/02/adding-analytics-to-my-blog/</link><pubDate>Wed, 18 Feb 2026 00:00:00 +0000</pubDate><guid>https://rogs.me/2026/02/adding-analytics-to-my-blog/</guid><description>&lt;p>Hey everyone, quick heads up: I&amp;rsquo;m adding analytics to the blog.&lt;/p>
&lt;p>Before you reach for your adblocker, hear me out. I&amp;rsquo;m using &lt;a href="https://umami.is/">Umami&lt;/a>, which is open source, privacy-respecting, and doesn&amp;rsquo;t use cookies. It doesn&amp;rsquo;t track you across sites, doesn&amp;rsquo;t collect personal data, and is fully &lt;a href="https://github.com/umami-software/umami">open source&lt;/a> so you can verify that yourself.&lt;/p>
&lt;p>On top of that, I&amp;rsquo;m self-hosting it on my own infrastructure, so the data never touches a third party. No Google Analytics, no Cloudflare analytics, no one else sees anything.&lt;/p>
&lt;p>I mainly want to know which posts are actually useful to people and which ones are just me yelling into the void. That&amp;rsquo;s it.&lt;/p>
&lt;p>If you have any questions or concerns, you know where to find me on the &lt;a href="https://rogs.me/contact">Contact&lt;/a> page.&lt;/p></description></item><item><title>Use your Claude Max subscription as an API with CLIProxyAPI</title><link>https://rogs.me/2026/02/use-your-claude-max-subscription-as-an-api-with-cliproxyapi/</link><pubDate>Fri, 13 Feb 2026 00:00:00 +0000</pubDate><guid>https://rogs.me/2026/02/use-your-claude-max-subscription-as-an-api-with-cliproxyapi/</guid><description>&lt;p>So here&amp;rsquo;s the thing: I&amp;rsquo;m paying $100/month for Claude Max. I use it a lot, it&amp;rsquo;s
worth it. But then I wanted to use my subscription with my Emacs packages —
specifically &lt;a href="https://gitlab.com/rogs/forge-llm">forge-llm&lt;/a> (which I wrote!) for generating PR descriptions in Forge,
and &lt;a href="https://github.com/douo/magit-gptcommit">magit-gptcommit&lt;/a> for auto-generating commit messages in Magit. Both packages
use the &lt;a href="https://elpa.gnu.org/packages/llm.html">llm&lt;/a> package, which supports OpenAI-compatible endpoints.&lt;/p>
&lt;p>The problem? Anthropic blocks OAuth tokens from being used directly with
third-party API clients. You &lt;em>have&lt;/em> to pay for API access separately. 🤔&lt;/p>
&lt;p>That felt wrong. I&amp;rsquo;m already paying for the subscription, why can&amp;rsquo;t I use it
however I want?&lt;/p>
&lt;p>Turns out, there&amp;rsquo;s a workaround. The Claude Code CLI &lt;em>can&lt;/em> use OAuth tokens.
So if you put a proxy in front of it that speaks the OpenAI API format, you can
use your Max subscription with basically anything that supports OpenAI
endpoints. And that&amp;rsquo;s exactly what &lt;a href="https://github.com/router-for-me/CLIProxyAPI">CLIProxyAPI&lt;/a> does.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil">Your App (Emacs llm package, scripts, whatever)
↓
HTTP Request (OpenAI format)
↓
CLIProxyAPI
↓
OAuth Token (from your Max subscription)
↓
Anthropic API
↓
Response → OpenAI format → Your App
&lt;/code>&lt;/pre>&lt;p>No extra API costs. Just your existing subscription. Sweet!&lt;/p>
&lt;h2 id="why-cliproxyapi-and-not-something-else">Why CLIProxyAPI and not something else?&lt;/h2>
&lt;p>I actually tried &lt;a href="https://github.com/atalovesyou/claude-max-api-proxy">claude-max-api-proxy&lt;/a> first. It worked! But the model list was
outdated (no Opus 4.5, no Sonnet 4.5), it&amp;rsquo;s a Node.js project that wraps the
CLI as a subprocess, and it felt a bit&amp;hellip; abandoned.&lt;/p>
&lt;p>CLIProxyAPI is a completely different story:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Single Go binary&lt;/strong>. No Node.js, no Python, no runtime dependencies. Just
download and run.&lt;/li>
&lt;li>&lt;strong>Actively maintained&lt;/strong>. Like, &lt;em>very&lt;/em> actively. Frequent releases, big
community, ecosystem tools everywhere (desktop GUI, web dashboard, AUR
package, Docker images, the works).&lt;/li>
&lt;li>&lt;strong>Multi-provider&lt;/strong>. Not just Claude: it also supports Gemini, OpenAI Codex,
Qwen, and more. You can even round-robin between multiple OAuth accounts.&lt;/li>
&lt;li>&lt;strong>All the latest models&lt;/strong>. It uses the full dated model names (e.g.,
&lt;code>claude-sonnet-4-20250514&lt;/code>), so you&amp;rsquo;re always up to date.&lt;/li>
&lt;/ul>
&lt;h2 id="what-you-ll-need">What you&amp;rsquo;ll need&lt;/h2>
&lt;ul>
&lt;li>An active &lt;strong>Claude Max subscription&lt;/strong> ($100/month). Claude Pro works too, but
with lower rate limits.&lt;/li>
&lt;li>A machine running &lt;strong>Linux&lt;/strong> or &lt;strong>macOS&lt;/strong>.&lt;/li>
&lt;li>A web browser for the OAuth flow (or use &lt;code>--no-browser&lt;/code> if you&amp;rsquo;re on a
headless server).&lt;/li>
&lt;/ul>
&lt;h2 id="installation">Installation&lt;/h2>
&lt;h3 id="linux">Linux&lt;/h3>
&lt;p>There&amp;rsquo;s a community installer that does everything for you: downloads the latest
binary to &lt;code>~/cliproxyapi/&lt;/code>, generates API keys, creates a systemd service:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="macos">macOS&lt;/h3>
&lt;p>Homebrew. Easy:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>brew install cliproxyapi
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="authenticating-with-claude">Authenticating with Claude&lt;/h2>
&lt;p>Before the proxy can use your subscription, you need to log in:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Linux&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd ~/cliproxyapi
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>./cli-proxy-api --claude-login
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># macOS (Homebrew)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cliproxyapi --claude-login
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This opens your browser for the OAuth flow. Log in with your Claude account,
authorize it, done. The token gets saved to &lt;code>~/.cli-proxy-api/&lt;/code>.&lt;/p>
&lt;p>If you&amp;rsquo;re on a headless machine, add &lt;code>--no-browser&lt;/code> and it&amp;rsquo;ll print the URL for
you to open elsewhere:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>./cli-proxy-api --claude-login --no-browser
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="configuration">Configuration&lt;/h2>
&lt;p>The installer generates a &lt;code>config.yaml&lt;/code> with random API keys. These are keys
that &lt;em>clients&lt;/em> use to authenticate to your proxy, not Anthropic keys.&lt;/p>
&lt;p>Here&amp;rsquo;s what I&amp;rsquo;m running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Bind to localhost only since I&amp;#39;m using it locally&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">host&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;127.0.0.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Server port&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">port&lt;/span>: &lt;span style="color:#ae81ff">8317&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Authentication directory&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">auth-dir&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;~/.cli-proxy-api&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># No client auth needed for local-only use&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">api-keys&lt;/span>: []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Keep it quiet&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">debug&lt;/span>: &lt;span style="color:#66d9ef">false&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The important bit is &lt;code>api-keys: []&lt;/code>. Setting it to an empty list disables
client authentication, which means any app on your machine can hit the proxy
without needing a key. This is fine if you&amp;rsquo;re only using it locally.&lt;/p>
&lt;p>If you&amp;rsquo;re exposing the proxy to your network (e.g., you want to hit it from
your phone or another machine), &lt;strong>keep the generated API keys&lt;/strong> and also set
&lt;code>host: &amp;quot;&amp;quot;&lt;/code> so it binds to all interfaces. You don&amp;rsquo;t want random people on your
network burning through your subscription.&lt;/p>
&lt;h2 id="starting-the-service">Starting the service&lt;/h2>
&lt;h3 id="linux--systemd">Linux (systemd)&lt;/h3>
&lt;p>The installer creates a systemd user service for you:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>systemctl --user enable --now cliproxyapi.service
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>systemctl --user status cliproxyapi.service
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or just run it manually to test first:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>cd ~/cliproxyapi
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>./cli-proxy-api
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="macos--homebrew">macOS (Homebrew)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>brew services start cliproxyapi
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="testing-it">Testing it&lt;/h2>
&lt;p>Let&amp;rsquo;s make sure everything works:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># List available models&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl http://localhost:8317/v1/models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Chat completion&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl -X POST http://localhost:8317/v1/chat/completions &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -H &lt;span style="color:#e6db74">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -d &lt;span style="color:#e6db74">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;model&amp;#34;: &amp;#34;claude-sonnet-4-20250514&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Say hello in one sentence.&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Streaming (note the -N flag to disable curl buffering)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl -N -X POST http://localhost:8317/v1/chat/completions &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -H &lt;span style="color:#e6db74">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -d &lt;span style="color:#e6db74">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;model&amp;#34;: &amp;#34;claude-sonnet-4-20250514&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Say hello in one sentence.&amp;#34;}],
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;stream&amp;#34;: true
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you get a response from Claude, you&amp;rsquo;re golden. 🎉&lt;/p>
&lt;h2 id="using-it-with-emacs">Using it with Emacs&lt;/h2>
&lt;p>This is the fun part. Both forge-llm and magit-gptcommit use the &lt;a href="https://elpa.gnu.org/packages/llm.html">llm&lt;/a> package
for their LLM backend. The &lt;code>llm&lt;/code> package has an OpenAI-compatible provider, so
we just need to point it at our proxy.&lt;/p>
&lt;h3 id="setting-up-the-llm-provider">Setting up the llm provider&lt;/h3>
&lt;p>First, make sure you have the &lt;code>llm&lt;/code> package installed. Then configure an OpenAI
provider that points to CLIProxyAPI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-emacs-lisp" data-lang="emacs-lisp">&lt;span style="display:flex;">&lt;span>(require &lt;span style="color:#e6db74">&amp;#39;llm-openai&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(setq my/claude-via-proxy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (make-llm-openai-compatible
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> :key &lt;span style="color:#e6db74">&amp;#34;not-needed&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> :chat-model &lt;span style="color:#e6db74">&amp;#34;claude-sonnet-4-20250514&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> :url &lt;span style="color:#e6db74">&amp;#34;http://localhost:8317/v1&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. That&amp;rsquo;s the whole LLM setup. Now we can use it everywhere.&lt;/p>
&lt;h3 id="forge-llm--pr-descriptions">forge-llm (PR descriptions)&lt;/h3>
&lt;p>I wrote &lt;a href="https://gitlab.com/rogs/forge-llm">forge-llm&lt;/a> to generate PR descriptions in Forge using LLMs. It
analyzes the git diff, picks up your repository&amp;rsquo;s PR template, and generates a
structured description. To use it with CLIProxyAPI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-emacs-lisp" data-lang="emacs-lisp">&lt;span style="display:flex;">&lt;span>(use-package forge-llm
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> :after forge
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> :config
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (forge-llm-setup)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (setq forge-llm-llm-provider my/claude-via-proxy))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now when you&amp;rsquo;re creating a PR in Forge, you can hit &lt;code>SPC m g&lt;/code> (Doom) or run
&lt;code>forge-llm-generate-pr-description&lt;/code> and Claude will write the description based
on your diff. Using your subscription. No API key needed.&lt;/p>
&lt;h3 id="magit-gptcommit--commit-messages">magit-gptcommit (commit messages)&lt;/h3>
&lt;p>&lt;a href="https://github.com/douo/magit-gptcommit">magit-gptcommit&lt;/a> does the same thing but for commit messages. It looks at your
staged changes and generates a conventional commit message. Setup:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-emacs-lisp" data-lang="emacs-lisp">&lt;span style="display:flex;">&lt;span>(use-package magit-gptcommit
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> :after magit
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> :config
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (setq magit-gptcommit-llm-provider my/claude-via-proxy)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (magit-gptcommit-mode &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (magit-gptcommit-status-buffer-setup))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now in the Magit commit buffer, you can generate a commit message with Claude.
Again, no separate API costs.&lt;/p>
&lt;h3 id="any-other-llm-based-package">Any other llm-based package&lt;/h3>
&lt;p>The beauty of the &lt;code>llm&lt;/code> package is that any Emacs package that uses it can
benefit from this setup. Just pass &lt;code>my/claude-via-proxy&lt;/code> as the provider. Some
other packages that use &lt;code>llm&lt;/code>: &lt;a href="https://github.com/s-kostyaev/ellama">ellama&lt;/a>, &lt;a href="https://github.com/ahyatt/ekg">ekg&lt;/a>, &lt;a href="https://github.com/akirak/llm-refactoring">llm-refactoring&lt;/a>. They&amp;rsquo;ll all
work with your Max subscription through the proxy.&lt;/p>
&lt;h2 id="using-it-with-other-tools">Using it with other tools&lt;/h2>
&lt;p>Since CLIProxyAPI speaks the OpenAI API format, it works with anything that
supports custom OpenAI endpoints. The magic three settings are always the same:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Base URL&lt;/strong>: &lt;code>http://localhost:8317/v1&lt;/code>&lt;/li>
&lt;li>&lt;strong>API key&lt;/strong>: &lt;code>not-needed&lt;/code> (or your proxy key if you have auth enabled)&lt;/li>
&lt;li>&lt;strong>Model&lt;/strong>: &lt;code>claude-sonnet-4-20250514&lt;/code>, &lt;code>claude-opus-4-20250514&lt;/code>, etc.&lt;/li>
&lt;/ul>
&lt;p>Here&amp;rsquo;s a Python example using the OpenAI SDK:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> openai &lt;span style="color:#f92672">import&lt;/span> OpenAI
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>client &lt;span style="color:#f92672">=&lt;/span> OpenAI(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> base_url&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;http://localhost:8317/v1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> api_key&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;not-needed&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>response &lt;span style="color:#f92672">=&lt;/span> client&lt;span style="color:#f92672">.&lt;/span>chat&lt;span style="color:#f92672">.&lt;/span>completions&lt;span style="color:#f92672">.&lt;/span>create(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;claude-sonnet-4-20250514&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> messages&lt;span style="color:#f92672">=&lt;/span>[{&lt;span style="color:#e6db74">&amp;#34;role&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;user&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;content&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Hello!&amp;#34;&lt;/span>}]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(response&lt;span style="color:#f92672">.&lt;/span>choices[&lt;span style="color:#ae81ff">0&lt;/span>]&lt;span style="color:#f92672">.&lt;/span>message&lt;span style="color:#f92672">.&lt;/span>content)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="available-models">Available models&lt;/h2>
&lt;p>CLIProxyAPI exposes all models available through your subscription. The names
use the full dated format. You can always check the list with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>curl -s http://localhost:8317/v1/models | jq &lt;span style="color:#e6db74">&amp;#39;.data[].id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>At the time of writing, you&amp;rsquo;ll get Claude Opus 4, Sonnet 4, Sonnet 4.5,
Haiku 4.5, and whatever else Anthropic has made available to Max subscribers.&lt;/p>
&lt;h2 id="how-much-does-this-save">How much does this save?&lt;/h2>
&lt;p>If you&amp;rsquo;re already paying for Claude Max, this is basically free API access.
For context:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Usage&lt;/th>
&lt;th>API Cost&lt;/th>
&lt;th>With CLIProxyAPI&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1M input tokens/month&lt;/td>
&lt;td>~$15&lt;/td>
&lt;td>$0 (included)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>500K output tokens/month&lt;/td>
&lt;td>~$37.50&lt;/td>
&lt;td>$0 (included)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Monthly Total&lt;/strong>&lt;/td>
&lt;td>&lt;strong>~$52.50&lt;/strong>&lt;/td>
&lt;td>&lt;strong>$0 extra&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>And those numbers add up quick when you&amp;rsquo;re generating PR descriptions and
commit messages all day. I was getting to the point where my API costs were
approaching the subscription price, which is silly when you think about it.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>The whole setup took me about 10 minutes. Download binary, authenticate, edit
config, start service, point my Emacs &lt;code>llm&lt;/code> provider at it. That&amp;rsquo;s it.&lt;/p>
&lt;p>What I love about CLIProxyAPI is that it&amp;rsquo;s exactly the kind of tool I
appreciate: a single binary, a YAML config, does one thing well, and gets out
of your way. No magic, no framework, no runtime dependencies. And since it&amp;rsquo;s
OpenAI-compatible, it plays nicely with the entire &lt;code>llm&lt;/code> package ecosystem in
Emacs.&lt;/p>
&lt;p>The project is at &lt;a href="https://github.com/router-for-me/CLIProxyAPI">https://github.com/router-for-me/CLIProxyAPI&lt;/a> and the
community is very active. If you run into issues, their GitHub issues are
responsive.&lt;/p>
&lt;p>See you in the next one!&lt;/p></description></item><item><title>Claude Code from the beach: My remote coding setup with mosh, tmux and ntfy</title><link>https://rogs.me/2026/02/claude-code-from-the-beach-my-remote-coding-setup-with-mosh-tmux-and-ntfy/</link><pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate><guid>https://rogs.me/2026/02/claude-code-from-the-beach-my-remote-coding-setup-with-mosh-tmux-and-ntfy/</guid><description>
&lt;a class="picture-link" href="https://rogs.me/1000121647.webp">
&lt;figure class="beach">&lt;img src="https://rogs.me/1000121647.webp">&lt;figcaption>The view two blocks from my apartment&lt;/figcaption>&lt;/figure>
&lt;/a>
&lt;p>I recently read &lt;a href="https://granda.org/en/2026/01/02/claude-code-on-the-go/">this awesome post&lt;/a> by Granda about running Claude Code from a
phone, and I thought: &lt;em>I need this in my life&lt;/em>. The idea is simple: kick off a
Claude Code task, pocket the phone, go do something fun, and get a notification
when Claude needs your help or finishes working. Async development from anywhere.&lt;/p>
&lt;p>But my setup is a bit different from his. I&amp;rsquo;m not using Tailscale or a cloud VM.
I already have a WireGuard VPN connecting my devices, a home server, and a
self-hosted ntfy instance. So I built my own version, tailored to my
infrastructure.&lt;/p>
&lt;p>Here&amp;rsquo;s the high-level architecture:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil">┌──────────┐ mosh ┌─────────────┐ ssh ┌─────────────┐
│ Phone │───────────────▶ │ Home Server │───────────────▶ │ Work PC │
│ (Termux) │ WireGuard │ (Jump Box) │ LAN │(Claude Code)│
└──────────┘ └─────────────┘ └──────┬──────┘
▲ │
│ ntfy (HTTPS) │
└─────────────────────────────────────────────────────────────┘
&lt;/code>&lt;/pre>&lt;p>The loop is: I&amp;rsquo;m at the beach, I type &lt;code>cc&lt;/code> on my phone, I land in a tmux session
with Claude Code. I give it a task, pocket the phone, and go back to whatever I
was doing. When Claude has a question or finishes, my phone buzzes. I pull it
out, respond, pocket it again. Development fits into the gaps of the day.&lt;/p>
&lt;p>And here&amp;rsquo;s what the async development loop looks like in practice:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil"> 📱 Phone 💻 Work PC 🔔 ntfy
│ │ │
│──── type &amp;#39;cc&amp;#39; ────────────▶│ │
│──── give Claude a task ───▶│ │
│ │ │
│ ┌─────────────────┐ │ │
│ │ pocket phone │ │ │
│ └─────────────────┘ │ │
│ │ │
│ │── hook fires ────────────▶│
│◀── &amp;#34;Claude needs input&amp;#34; ───────────────────────────────│
│ │ │
│──── respond ──────────────▶│ │
│ │ │
│ ┌─────────────────┐ │ │
│ │ pocket phone │ │ │
│ └─────────────────┘ │ │
│ │ │
│ │── hook fires ────────────▶│
│◀── &amp;#34;Task complete&amp;#34; ────────────────────────────────────│
│ │ │
│──── review, approve PR ───▶│ │
│ │ │
&lt;/code>&lt;/pre>&lt;h2 id="why-not-just-use-the-blog-post-s-setup">Why not just use the blog post&amp;rsquo;s setup?&lt;/h2>
&lt;p>Granda&amp;rsquo;s setup uses Tailscale for VPN, a Vultr cloud VM, Termius as the mobile
terminal, and Poke for notifications. It&amp;rsquo;s clean and it works. But I had
different constraints:&lt;/p>
&lt;ul>
&lt;li>I already have a &lt;strong>WireGuard VPN&lt;/strong> running &lt;code>wg-quick&lt;/code> on a server that connects all my devices. No need
for Tailscale.&lt;/li>
&lt;li>I didn&amp;rsquo;t want to pay for a cloud VM. My work PC is more than powerful enough to
run Claude Code.&lt;/li>
&lt;li>I self-host &lt;strong>ntfy&lt;/strong> for notifications, so no need for Poke or any external
notification service.&lt;/li>
&lt;li>I use &lt;strong>Termux&lt;/strong> (open-source), not Termius.&lt;/li>
&lt;/ul>
&lt;p>If you don&amp;rsquo;t have this kind of infrastructure already, Granda&amp;rsquo;s approach is
probably simpler. But if you&amp;rsquo;re the kind of person who already has a WireGuard
mesh and self-hosted services, this guide is for you.&lt;/p>
&lt;h2 id="the-pieces">The pieces&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Purpose&lt;/th>
&lt;th>Alternatives&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>WireGuard&lt;/td>
&lt;td>VPN to reach home network&lt;/td>
&lt;td>Tailscale, Zerotier, Nebula&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>mosh&lt;/td>
&lt;td>Network-resilient shell (phone leg)&lt;/td>
&lt;td>Eternal Terminal (et), plain SSH&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SSH&lt;/td>
&lt;td>Secure connection (LAN leg)&lt;/td>
&lt;td>mosh (if you want it end-to-end)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>tmux&lt;/td>
&lt;td>Session persistence&lt;/td>
&lt;td>screen, zellij&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Claude Code&lt;/td>
&lt;td>The actual work&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ntfy&lt;/td>
&lt;td>Push notifications&lt;/td>
&lt;td>Pushover, Gotify, Poke, Telegram&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Termux&lt;/td>
&lt;td>Terminal emulator&lt;/td>
&lt;td>Termius, JuiceSSH, ConnectBot&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>fish shell&lt;/td>
&lt;td>Shell on all machines&lt;/td>
&lt;td>zsh, bash&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The key insight is that you need &lt;strong>two different types of resilience&lt;/strong>: mosh
handles the flaky mobile connection (WiFi to cellular transitions, dead zones,
phone sleeping), while tmux handles session persistence (close the app, reopen
hours later, everything&amp;rsquo;s still there). Together they make mobile development
actually viable.&lt;/p>
&lt;h2 id="why-the-double-ssh-why-not-make-the-work-pc-a-wireguard-peer">Why the double SSH? Why not make the work PC a WireGuard peer?&lt;/h2>
&lt;p>You might be wondering: if I already have a WireGuard network, why not just add
the work PC as a peer and mosh straight into it from my phone?&lt;/p>
&lt;p>The short answer: &lt;strong>it&amp;rsquo;s my employer&amp;rsquo;s machine&lt;/strong>. It has monitoring software
installed: screen grabbing, endpoint policies, the works. Installing WireGuard
on it would mean running a VPN client that tunnels traffic through my personal
infrastructure, which is the kind of thing that raises flags with IT security. I
don&amp;rsquo;t want to deal with that conversation.&lt;/p>
&lt;p>SSH, on the other hand, is standard dev tooling. An openssh-server on a Linux
machine is about as unremarkable as it gets.&lt;/p>
&lt;p>So instead, my home server acts as a jump box. My phone connects to the home
server over WireGuard (that&amp;rsquo;s all personal infrastructure, no employer
involvement), and then the home server SSHs into the work PC over the local
network. The work PC only needs an SSH server, no VPN client, no weird tunnels,
nothing that would make the monitoring software blink.&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil"> ┌──────────────────────────────────────────────────┐
│ My Infrastructure │
│ │
│ ┌───────────┐ WireGuard ┌──────────────┐ │
│ │ Phone │◀──────────────▶│ WG Server │ │
│ │ (peer) │ tunnel │ │ │
│ └─────┬─────┘ └──────┬───────┘ │
│ │ │ │
│ │ mosh WireGuard │ │
│ │ (through tunnel) tunnel │ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ │
│ │ Home Server │◀───────────────────────────────│
│ │ (peer) │ │
│ └──────┬───────┘ │
│ │ │
└─────────┼────────────────────────────────────────┘
│
│ ssh (LAN)
│
┌─────────┼────────────────────────────────────────┐
│ ▼ │
│ ┌────────────┐ │
│ │ Work PC │ │
│ │ (SSH only) │ Employer Infrastructure │
│ └────────────┘ │
└──────────────────────────────────────────────────┘
&lt;/code>&lt;/pre>&lt;p>As a bonus, this means the work PC has zero exposure to the public internet. It
only accepts SSH from machines on my local network. Defense in depth.&lt;/p>
&lt;h2 id="phase-1-ssh-server-on-the-work-pc">Phase 1: SSH server on the work PC&lt;/h2>
&lt;p>My work PC is running Ubuntu 24.04. First thing: install and harden the SSH
server.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo apt update &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> sudo apt install -y openssh-server
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo systemctl enable ssh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Note: on Ubuntu 24.04 the service is called &lt;code>ssh&lt;/code>, not &lt;code>sshd&lt;/code>. This tripped me
up.&lt;/p>
&lt;p>Then harden the config. I created &lt;code>/etc/ssh/sshd_config&lt;/code> with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>PermitRootLogin no
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PasswordAuthentication no
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>KbdInteractiveAuthentication no
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PubkeyAuthentication yes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AllowAgentForwarding no
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>X11Forwarding no
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>UsePAM yes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>MaxAuthTries &lt;span style="color:#ae81ff">3&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ClientAliveInterval &lt;span style="color:#ae81ff">60&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ClientAliveCountMax &lt;span style="color:#ae81ff">3&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Key-only auth, no root login, no password auth. Since the machine is only
accessible through my local network, this is plenty secure.&lt;/p>
&lt;h3 id="setting-up-ssh-keys-for-the-home-server-work-pc-connection">Setting up SSH keys for the home server → work PC connection&lt;/h3>
&lt;p>On the &lt;strong>home server&lt;/strong>, generate a key pair if you don&amp;rsquo;t already have one:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ssh-keygen -t ed25519 -C &lt;span style="color:#e6db74">&amp;#34;homeserver-&amp;gt;workpc&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Accept the default path (&lt;code>/.ssh/id_ed25519&lt;/code>). Then copy the public key to the
work PC:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ssh-copy-id roger@&amp;lt;work-pc-ip&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now restart sshd:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo systemctl restart ssh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Important&lt;/strong>: Test the SSH connection from your home server &lt;em>before&lt;/em> closing your
current session. Don&amp;rsquo;t lock yourself out.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># From the home server&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ssh roger@&amp;lt;work-pc-ip&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If it drops you into a shell without asking for a password, you&amp;rsquo;re golden.&lt;/p>
&lt;h3 id="alternative-tailscale">Alternative: Tailscale&lt;/h3>
&lt;p>If you don&amp;rsquo;t have a WireGuard setup, &lt;a href="https://tailscale.com/">Tailscale&lt;/a> is the easiest way to get a
private network going. Install it on your phone and your work PC, and they can
see each other directly. No jump host needed, no port forwarding, no firewall
rules. It&amp;rsquo;s honestly magic for this kind of thing. The only reason I don&amp;rsquo;t use it
is because I already had WireGuard running before Tailscale existed.&lt;/p>
&lt;h2 id="phase-2-tmux-plus-auto-attach">Phase 2: tmux + auto-attach&lt;/h2>
&lt;p>The idea here is simple: every time I SSH into the work PC, I want to land
directly in a tmux session. If the session already exists, attach to it. If not,
create one.&lt;/p>
&lt;p>First, &lt;code>~/.tmux.conf&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># mouse support (essential for thumbing it on the phone)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>set -g mouse on
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># start window numbering at 1 (easier to reach on phone keyboard)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>set -g base-index &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>setw -g pane-base-index &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># status bar&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>set -g status-style &lt;span style="color:#e6db74">&amp;#39;bg=colour235 fg=colour136&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>set -g status-left &lt;span style="color:#e6db74">&amp;#39;#[fg=colour46][#S] &amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>set -g status-right &lt;span style="color:#e6db74">&amp;#39;#[fg=colour166]%H:%M&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>set -g status-left-length &lt;span style="color:#ae81ff">30&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># longer scrollback&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>set -g history-limit &lt;span style="color:#ae81ff">50000&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># reduce escape delay (makes editors snappier over SSH)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>set -sg escape-time &lt;span style="color:#ae81ff">10&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># keep sessions alive&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>set -g destroy-unattached off
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Mouse support is &lt;strong>essential&lt;/strong> when you&amp;rsquo;re using your phone. Being able to tap to
select panes, scroll with your finger, and resize things makes a massive
difference.&lt;/p>
&lt;p>Then in &lt;code>~/.config/fish/config.fish&lt;/code> on the work PC:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fish" data-lang="fish">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">set&lt;/span> &lt;span style="color:#a6e22e">-q&lt;/span> SSH_CONNECTION; &lt;span style="color:#66d9ef">and&lt;/span> &lt;span style="color:#66d9ef">not&lt;/span> &lt;span style="color:#66d9ef">set&lt;/span> &lt;span style="color:#a6e22e">-q&lt;/span> TMUX
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">tmux&lt;/span> attach &lt;span style="color:#a6e22e">-t&lt;/span> claude &lt;span style="color:#ae81ff">2&lt;/span>&lt;span style="color:#f92672">&amp;gt;&lt;/span>/dev/null; &lt;span style="color:#66d9ef">or&lt;/span> &lt;span style="color:#a6e22e">tmux&lt;/span> new &lt;span style="color:#a6e22e">-s&lt;/span> claude &lt;span style="color:#a6e22e">-c&lt;/span> ~/projects/my-app
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This checks for &lt;code>SSH_CONNECTION&lt;/code> so it only auto-attaches when I&amp;rsquo;m remoting in.
When I&amp;rsquo;m physically at the machine, I use the terminal normally without tmux.
This distinction becomes important later for notifications.&lt;/p>
&lt;h2 id="phase-3-claude-code-hooks-plus-ntfy">Phase 3: Claude Code hooks + ntfy&lt;/h2>
&lt;p>This is the fun part. Claude Code has a &lt;a href="https://docs.anthropic.com/en/docs/claude-code/hooks">hook system&lt;/a> that lets you run commands
when certain events happen. We&amp;rsquo;re going to hook into three events:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>AskUserQuestion&lt;/strong>: Claude needs my input. High priority notification.&lt;/li>
&lt;li>&lt;strong>Stop&lt;/strong>: Claude finished the task. Normal priority.&lt;/li>
&lt;li>&lt;strong>Error&lt;/strong>: Something broke. High priority.&lt;/li>
&lt;/ul>
&lt;h3 id="the-notification-script">The notification script&lt;/h3>
&lt;p>First, the script that sends notifications. I created
&lt;code>~/.claude/hooks/notify.sh&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/usr/bin/env bash
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Only notify if we&amp;#39;re in an SSH-originated tmux session&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> ! tmux show-environment SSH_CONNECTION 2&amp;gt;/dev/null | grep -q SSH_CONNECTION&lt;span style="color:#f92672">=&lt;/span>; &lt;span style="color:#66d9ef">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> exit &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>EVENT_TYPE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>1&lt;span style="color:#66d9ef">:-&lt;/span>unknown&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>NTFY_URL&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://ntfy.example.com/claude-code&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>NTFY_TOKEN&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;tk_your_token_here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>EVENT_DATA&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>cat&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$EVENT_TYPE&lt;span style="color:#e6db74">&amp;#34;&lt;/span> in
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> question&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TITLE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;🤔 Claude needs input&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PRIORITY&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;high&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MESSAGE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>echo &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$EVENT_DATA&lt;span style="color:#e6db74">&amp;#34;&lt;/span> | jq -r &lt;span style="color:#e6db74">&amp;#39;.tool_input.question // .tool_input.questions[0].question // &amp;#34;Claude has a question for you&amp;#34;&amp;#39;&lt;/span> 2&amp;gt;/dev/null&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ;;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> stop&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TITLE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;✅ Claude finished&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PRIORITY&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;default&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MESSAGE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Task complete&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ;;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> error&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TITLE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;❌ Claude hit an error&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PRIORITY&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;high&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MESSAGE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>echo &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$EVENT_DATA&lt;span style="color:#e6db74">&amp;#34;&lt;/span> | jq -r &lt;span style="color:#e6db74">&amp;#39;.error // &amp;#34;Something went wrong&amp;#34;&amp;#39;&lt;/span> 2&amp;gt;/dev/null&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ;;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> *&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TITLE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Claude Code&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PRIORITY&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;default&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MESSAGE&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Event: &lt;/span>$EVENT_TYPE&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ;;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">esac&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PROJECT&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>basename &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$PWD&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl -s &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -H &lt;span style="color:#e6db74">&amp;#34;Authorization: Bearer &lt;/span>$NTFY_TOKEN&lt;span style="color:#e6db74">&amp;#34;&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -H &lt;span style="color:#e6db74">&amp;#34;Title: &lt;/span>$TITLE&lt;span style="color:#e6db74">&amp;#34;&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -H &lt;span style="color:#e6db74">&amp;#34;Priority: &lt;/span>$PRIORITY&lt;span style="color:#e6db74">&amp;#34;&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -H &lt;span style="color:#e6db74">&amp;#34;Tags: computer&amp;#34;&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -d &lt;span style="color:#e6db74">&amp;#34;[&lt;/span>$PROJECT&lt;span style="color:#e6db74">] &lt;/span>$MESSAGE&lt;span style="color:#e6db74">&amp;#34;&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$NTFY_URL&lt;span style="color:#e6db74">&amp;#34;&lt;/span> &amp;gt; /dev/null 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>chmod +x ~/.claude/hooks/notify.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>SSH_CONNECTION&lt;/code> check at the top is crucial: it prevents notifications from
firing when I&amp;rsquo;m sitting at the machine. Since I only use tmux when SSHing in
remotely, the tmux environment will only have &lt;code>SSH_CONNECTION&lt;/code> set when I&amp;rsquo;m
remote. Neat trick.&lt;/p>
&lt;h3 id="claude-code-settings">Claude Code settings&lt;/h3>
&lt;p>Then in &lt;code>~/.claude/settings.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;hooks&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;PreToolUse&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;matcher&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;AskUserQuestion&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;hooks&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;command&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;command&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;~/.claude/hooks/notify.sh question&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;Stop&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;hooks&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;command&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;command&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;~/.claude/hooks/notify.sh stop&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is the global settings file. If your project also has a
&lt;code>.claude/settings.json&lt;/code>, they&amp;rsquo;ll be merged. No conflicts.&lt;/p>
&lt;h3 id="ntfy-setup">ntfy setup&lt;/h3>
&lt;p>I&amp;rsquo;m self-hosting ntfy, so I created a topic and an access token:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Inside your ntfy server/container&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ntfy token add --expires&lt;span style="color:#f92672">=&lt;/span>30d your-username
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ntfy access your-username claude-code rw
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ntfy access everyone claude-code deny
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ntfy topics are created on demand, so just subscribing to one creates it. On the
Android ntfy app, I pointed it at my self-hosted instance and subscribed to the
&lt;code>claude-code&lt;/code> topic.&lt;/p>
&lt;p>You can test the whole thing works with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#39;{&amp;#34;tool_input&amp;#34;:{&amp;#34;question&amp;#34;:&amp;#34;Should I refactor this?&amp;#34;}}&amp;#39;&lt;/span> | ~/.claude/hooks/notify.sh question
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#39;{}&amp;#39;&lt;/span> | ~/.claude/hooks/notify.sh stop
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#39;{&amp;#34;error&amp;#34;:&amp;#34;ModuleNotFoundError: No module named foo&amp;#34;}&amp;#39;&lt;/span> | ~/.claude/hooks/notify.sh error
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Three notifications, three different priorities. Very satisfying.&lt;/p>
&lt;h3 id="alternative-notification-systems">Alternative notification systems&lt;/h3>
&lt;p>If you don&amp;rsquo;t want to self-host ntfy, here are some options:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://ntfy.sh">ntfy.sh&lt;/a>&lt;/strong>: The public instance of ntfy. Free, no setup, just pick a
random-ish topic name. The downside is that anyone who knows your topic name
can send you notifications.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://pushover.net/">Pushover&lt;/a>&lt;/strong>: $5 one-time purchase per platform. Very reliable, nice API. The
notification script would be almost identical, just a different curl call.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://gotify.net/">Gotify&lt;/a>&lt;/strong>: Self-hosted like ntfy, but uses WebSockets instead of HTTP. Good if
you&amp;rsquo;re already running it.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://core.telegram.org/bots/api">Telegram Bot API&lt;/a>&lt;/strong>: Free, easy to set up. Create a bot with BotFather, get
your chat ID, and curl the sendMessage endpoint.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://poke.dev/">Poke&lt;/a>&lt;/strong>: What Granda uses in his post. Simple webhook-to-push service.&lt;/li>
&lt;/ul>
&lt;h2 id="phase-4-termux-setup">Phase 4: Termux setup&lt;/h2>
&lt;p>Termux is the terminal emulator on my Android phone. Here&amp;rsquo;s how I set it up.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>pkg update &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> pkg install -y mosh openssh fish
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="ssh-into-your-phone--for-easier-setup">SSH into your phone (for easier setup)&lt;/h3>
&lt;p>Configuring all of this on a phone keyboard is painful. I set up sshd on Termux
so I could configure it from my PC.&lt;/p>
&lt;p>In &lt;code>~/.config/fish/config.fish&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fish" data-lang="fish">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">sshd&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>&lt;span style="color:#f92672">&amp;gt;&lt;/span>/dev/null
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This starts sshd every time you open Termux. If it&amp;rsquo;s already running, it
silently fails. Termux runs sshd on port 8022 by default.&lt;/p>
&lt;p>First, set a password on Termux (you&amp;rsquo;ll need it for the initial key copy):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>passwd
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then from your PC, copy your key and test the connection:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ssh-copy-id -p &lt;span style="color:#ae81ff">8022&lt;/span> &amp;lt;phone-ip&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ssh -p &lt;span style="color:#ae81ff">8022&lt;/span> &amp;lt;phone-ip&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now you can configure Termux comfortably from your PC keyboard.&lt;/p>
&lt;h3 id="generating-ssh-keys-on-the-phone">Generating SSH keys on the phone&lt;/h3>
&lt;p>On Termux, generate a key pair:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ssh-keygen -t ed25519 -C &lt;span style="color:#e6db74">&amp;#34;phone&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then copy it to your home server:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ssh-copy-id &amp;lt;your-user&amp;gt;@&amp;lt;home-server-wireguard-ip&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This gives you passwordless &lt;code>phone → home server&lt;/code>. Since we already set up
&lt;code>home server → work PC&lt;/code> keys in Phase 1, the full chain is now passwordless.&lt;/p>
&lt;h3 id="ssh-config">SSH config&lt;/h3>
&lt;p>The SSH config is where the magic happens. On Termux:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil">Host home
HostName &amp;lt;home-server-wireguard-ip&amp;gt;
User &amp;lt;your-user&amp;gt;
Host work
HostName &amp;lt;work-pc-ip&amp;gt;
User roger
ProxyJump home
&lt;/code>&lt;/pre>&lt;p>&lt;code>ProxyJump&lt;/code> is the key: &lt;code>ssh work&lt;/code> automatically hops through the home server.
No manual double-SSHing.&lt;/p>
&lt;h3 id="fish-aliases">Fish aliases&lt;/h3>
&lt;p>These are the aliases that make everything a one-command operation:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fish" data-lang="fish">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Connect to work PC, land in tmux with Claude Code ready
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>alias cc&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;mosh home -- ssh -t work&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># New tmux window in the claude session
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>alias cn&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;mosh home -- ssh -t work &amp;#39;tmux new-window -t claude -c \$HOME/projects/my-app&amp;#39;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># List tmux windows
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>alias cl&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;ssh work &amp;#39;tmux list-windows -t claude&amp;#39;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>cc&lt;/code> is all I need to type. Mosh handles the phone-to-home-server connection
(surviving WiFi/cellular transitions), SSH handles the home-server-to-work-PC
hop over the LAN, and the fish config on the work PC auto-attaches to tmux.&lt;/p>
&lt;h3 id="alternative-termius">Alternative: Termius&lt;/h3>
&lt;p>If you&amp;rsquo;re on iOS (or just prefer a polished app), &lt;a href="https://termius.com/">Termius&lt;/a> is what Granda uses.
It supports mosh natively and has a nice UI. The downside is it&amp;rsquo;s a subscription
for the full features. Termux is free and open-source, and gives you a full Linux environment.&lt;/p>
&lt;p>Other options: &lt;a href="https://juicessh.com/">JuiceSSH&lt;/a> (Android, no mosh), &lt;a href="https://connectbot.org/">ConnectBot&lt;/a> (Android, no mosh).
Mosh support is really the killer feature here, so Termux or Termius are the
best choices.&lt;/p>
&lt;h2 id="phase-5-the-full-flow">Phase 5: The full flow&lt;/h2>
&lt;p>Here&amp;rsquo;s what my actual workflow looks like:&lt;/p>
&lt;ol>
&lt;li>I&amp;rsquo;m at the beach/coffee shop/couch/wherever 🏖️&lt;/li>
&lt;li>Open Termux, type &lt;code>cc&lt;/code>&lt;/li>
&lt;li>I&amp;rsquo;m in my tmux session on my work PC&lt;/li>
&lt;li>Start Claude Code, give it a task: &amp;ldquo;add pagination to the user dashboard API
and update the tests&amp;rdquo;&lt;/li>
&lt;li>Pocket the phone&lt;/li>
&lt;li>Phone buzzes: &amp;ldquo;🤔 Claude needs input — Should I use cursor-based or
offset-based pagination?&amp;rdquo;&lt;/li>
&lt;li>Pull out phone, Termux is still connected (thanks mosh), type &amp;ldquo;cursor-based,
use the created_at field&amp;rdquo;&lt;/li>
&lt;li>Pocket the phone again&lt;/li>
&lt;li>Phone buzzes: &amp;ldquo;✅ Claude finished — Task complete&amp;rdquo;&lt;/li>
&lt;li>Review the changes, approve the PR, go back to the beach&lt;/li>
&lt;/ol>
&lt;p>The key thing that makes this work is the combination of &lt;strong>mosh&lt;/strong> (connection
survives me pocketing the phone) + &lt;strong>tmux&lt;/strong> (session survives even if mosh dies) +
&lt;strong>ntfy&lt;/strong> (I don&amp;rsquo;t have to keep checking the screen). Without any one of these
three, the experience breaks down.&lt;/p>
&lt;h2 id="security-considerations">Security considerations&lt;/h2>
&lt;p>A few things to keep in mind:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>SSH keys only&lt;/strong>: No password auth anywhere in the chain. Keys are easier to
manage and impossible to brute force.&lt;/li>
&lt;li>&lt;strong>WireGuard&lt;/strong>: The work PC is only accessible through my local network. No ports
exposed to the public internet.&lt;/li>
&lt;li>&lt;strong>ntfy token auth&lt;/strong>: The notification topic requires authentication. No one else
can send you fake notifications or read your Claude Code questions.&lt;/li>
&lt;li>&lt;strong>Claude Code in normal mode&lt;/strong>: Unlike Granda&amp;rsquo;s setup where he runs permissive
mode on a disposable VM, my work PC is &lt;em>not&lt;/em> disposable. Claude asks before
running dangerous commands, which pairs nicely with the notification system.&lt;/li>
&lt;li>&lt;strong>tmux SSH check&lt;/strong>: Notifications only fire when I&amp;rsquo;m remote. When I&amp;rsquo;m at the
machine, no unnecessary pings.&lt;/li>
&lt;/ul>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>The whole setup took me about an hour to put together. The actual configuration
is pretty minimal: an SSH server, a tmux config, a notification script, and some
fish aliases.&lt;/p>
&lt;p>What I love about this setup is that it&amp;rsquo;s &lt;strong>all stuff I already had&lt;/strong>. WireGuard
was already running, ntfy was already self-hosted, Termux was already on my
phone. I just wired them together with a few scripts and some Claude Code hooks.&lt;/p>
&lt;p>If you have a similar homelab setup, you can probably get this running in 30
minutes. If you&amp;rsquo;re starting from scratch, Granda&amp;rsquo;s &lt;a href="https://granda.org/en/2026/01/02/claude-code-on-the-go/">cloud VM approach&lt;/a> is probably
easier. Either way, async coding from your phone is genuinely a game changer.&lt;/p>
&lt;p>See you in the next one!&lt;/p></description></item><item><title>Introducing: YAMS (Yet Another Media Server)!</title><link>https://rogs.me/2023/01/introducing-yams-yet-another-media-server/</link><pubDate>Fri, 20 Jan 2023 09:57:48 -0300</pubDate><guid>https://rogs.me/2023/01/introducing-yams-yet-another-media-server/</guid><description>&lt;p>Hello internet 😎&lt;/p>
&lt;p>I&amp;rsquo;m here with a &lt;strong>big&lt;/strong> announcement: I have created a bash script that installs my entire media server,
fast and easy 🎉&lt;/p>
&lt;figure>&lt;img src="https://yams.media/install-yams.gif"/>
&lt;/figure>
&lt;h2 id="tl-dr">TL;DR&lt;/h2>
&lt;p>I&amp;rsquo;ve created YAMS. A full media server that allows you to download and categorize your shows/movies.&lt;/p>
&lt;p>Go to YAMS&amp;rsquo;s website here: &lt;a href="http://yams.media">http://yams.media&lt;/a> or check it on Gitlab here: &lt;a href="https://gitlab.com/rogs/yams">https://gitlab.com/rogs/yams&lt;/a>.&lt;/p>
&lt;h2 id="a-little-history">A little history&lt;/h2>
&lt;p>When I first set up my media server, it took me ~2 weeks to install, configure and understand how it&amp;rsquo;s
supposed to work: Linking Sonarr, Radarr, Jackett together, choosing a good BitTorrent downloader,
understanding all the moving pieces, choosing Emby, etc. My plan with YAMS is to make it easier
for noobs (and lazy people like me) to set up their media servers super easily.&lt;/p>
&lt;p>I have been working on YAMS for ~2 weeks. The docker-compose file has existed for almost 2 years but
without any configuration instructions. Basically, you had to do everything manually, and if you didn&amp;rsquo;t
have any experience with docker, docker-compose, or any of the services included, it was very cumbersome
to configure and understand how everything worked together.&lt;/p>
&lt;p>So basically, I&amp;rsquo;m encapsulating my experience for anyone that wants to use it. If you don&amp;rsquo;t like it, at
least you might learn something from my experience, YAMS&amp;rsquo;s &lt;a href="https://git.rogs.me/yams.git/tree/docker-compose.example.yaml">docker-compose file&lt;/a> or its &lt;a href="https://yams.media/config/">configuration
tutorial&lt;/a>.&lt;/p>
&lt;p>This is my first (and hopefully not last!) piece of open source software. I know it&amp;rsquo;s just a &lt;a href="https://git.rogs.me/yams.git/tree/install.sh">bash script&lt;/a>
that sets up a &lt;a href="https://git.rogs.me/yams.git/tree/docker-compose.example.yaml">docker-compose&lt;/a> file, but seeing how my friends are using it and giving me feedback is
exciting and addictive!&lt;/p>
&lt;h2 id="why">Why?&lt;/h2>
&lt;p>In 2019 I wanted a setup that my non-technical girlfriend could use without any problems, so I started
designing my media server using multiple open source projects and running them on top of docker.&lt;/p>
&lt;p>Today I would like to say it works very well 😎 And most importantly, I accomplished my goal: My
girlfriend uses it regularly and I even was able to expand it to my mother, who lives 5000kms from me.&lt;/p>
&lt;p>But then, my friends saw my setup&amp;hellip;&lt;/p>
&lt;p>On June 2022 I had a small &amp;ldquo;party&amp;rdquo; with my work friends at my apartment, and all of them were very
impressed with my home server setup:&lt;/p>
&lt;ul>
&lt;li>&amp;ldquo;Sonarr&amp;rdquo; to index shows.&lt;/li>
&lt;li>&amp;ldquo;Radarr&amp;rdquo; to index movies.&lt;/li>
&lt;li>&amp;ldquo;qBittorrent&amp;rdquo; to download torrents.&lt;/li>
&lt;li>&amp;ldquo;Emby&amp;rdquo; to serve the server.&lt;/li>
&lt;/ul>
&lt;p>They kept telling me to create a tutorial, or just teach them how to set one up themselves.&lt;/p>
&lt;p>I tried to explain the full setup to one of them, but explaining how everything connected and worked
together was a big pain. That is what led me to create this script and configuration tutorial, so anyone
regardless of their tech background and knowledge could start a basic media server.&lt;/p>
&lt;p>So basically, my friends pushed me to build this script and documentation, so they (and now anyone!)
could build it on their own home servers.&lt;/p>
&lt;h2 id="ok-sounds-cool-dot-what-did-you-do-then">Ok, sounds cool. What did you do then?&lt;/h2>
&lt;p>&lt;a href="https://git.rogs.me/yams.git/tree/install.sh">A bash script&lt;/a> that asks basic questions to the user and sets up the ultimate media server, with
&lt;a href="https://yams.media/config/">configuration instructions included&lt;/a>! (That&amp;rsquo;s the part I really &lt;strong>REALLY&lt;/strong> enjoyed!)&lt;/p>
&lt;h2 id="what-s-included-with-yams">What&amp;rsquo;s included with YAMS?&lt;/h2>
&lt;p>This script installs the following software:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://sonarr.tv/">Sonarr&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://radarr.video/">Radarr&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://emby.media/">Emby&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.qbittorrent.org/">qBittorrent&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.bazarr.media/">Bazarr&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/Jackett/Jackett">Jackett&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/qdm12/gluetun">gluetun&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>This combination allows you to create a fully functional media server that is going to download,
categorize, subtitle, and serve your favorite shows and movies.&lt;/p>
&lt;h2 id="features">Features&lt;/h2>
&lt;p>In no particular order:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Automatic shows/movies download&lt;/strong>: Just add your shows and movies to the watch list and it should
automatically download the files when they are available.&lt;/li>
&lt;li>&lt;strong>Automatic classification and organization&lt;/strong>: Your media files should be completely organized by default.&lt;/li>
&lt;li>&lt;strong>Automatic subtitles download&lt;/strong>: Self-explanatory. Your media server should automatically download
subtitles in the languages you choose if they are available.&lt;/li>
&lt;li>&lt;strong>Support for Web, Android, iOS, Android TV, and whatever that can support Emby&lt;/strong>: Since we are
using Emby, you should be able to watch your favorite media almost anywhere.&lt;/li>
&lt;/ul>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>You can go to YAMS&amp;rsquo;s website here: &lt;a href="https://yams.media">https://yams.media&lt;/a>.&lt;/p>
&lt;p>I&amp;rsquo;m &lt;strong>very&lt;/strong> proud of how YAMS is turning out! If you end up using it on your server, I just want to tell
you &lt;strong>THANK YOU&lt;/strong> 🙇 from the bottom of my heart. You are &lt;strong>&lt;strong>AWESOME!&lt;/strong>&lt;/strong>&lt;/p>
&lt;p>Feedback is GREATLY appreciated (the VPN was added from the feedback!). I&amp;rsquo;m here to support YAMS for the
long run, so I would like suggestions on how to improve the setup/website/configuration steps.&lt;/p>
&lt;p>You can always submit &lt;a href="https://gitlab.com/rogs/yams/-/issues/new">issues&lt;/a> on Gitlab if you find any problems, or you can &lt;a href="https://rogs.me/contact">contact&lt;/a> me directly (email
preferred!).&lt;/p></description></item><item><title>Removing comments from my blog</title><link>https://rogs.me/2023/01/removing-comments-from-my-blog/</link><pubDate>Sat, 14 Jan 2023 00:00:00 +0000</pubDate><guid>https://rogs.me/2023/01/removing-comments-from-my-blog/</guid><description>&lt;p>I&amp;rsquo;m removing comments from my blog.&lt;/p>
&lt;p>I&amp;rsquo;ve been thinking about this for a while, but I noticed that comments weren&amp;rsquo;t being used and most posts
were not that interesting. Don&amp;rsquo;t get me wrong, I really appreciate your awesome comments, but running
commento takes a lot of resources and I don&amp;rsquo;t really see the full benefit of them.&lt;/p>
&lt;p>From now on, if you want to leave a comment (&amp;ldquo;thank yous&amp;rdquo;, suggestions, etc), you can send me an email.
You&amp;rsquo;ll find my email addess on the &lt;a href="https://rogs.me/contact">Contact&lt;/a> page.&lt;/p>
&lt;p>You have a good and relevant comment, I&amp;rsquo;ll update the relevant post accordingly.&lt;/p></description></item><item><title>Using MinIO to upload to a local S3 bucket in Django</title><link>https://rogs.me/2021/01/using-minio-to-upload-to-a-local-s3-bucket-in-django/</link><pubDate>Sun, 10 Jan 2021 00:00:00 +0000</pubDate><guid>https://rogs.me/2021/01/using-minio-to-upload-to-a-local-s3-bucket-in-django/</guid><description>&lt;p>So MinIO its an object storage that uses the same API as S3, which means that we
can use the same S3 compatible libraries in Python, like &lt;a href="https://pypi.org/project/boto3/">Boto3&lt;/a> and &lt;a href="https://pypi.org/project/django-storages/">django-storages&lt;/a>.&lt;/p>
&lt;h2 id="the-setup">The setup&lt;/h2>
&lt;p>Here&amp;rsquo;s the docker-compose configuration for my django app:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">app&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">build&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">context&lt;/span>: &lt;span style="color:#ae81ff">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">./app:/app&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">8000&lt;/span>:&lt;span style="color:#ae81ff">8000&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">depends_on&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">minio&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: &amp;gt;&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> sh -c &amp;#34;python manage.py migrate &amp;amp;&amp;amp;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> python manage.py runserver 0.0.0.0:8000&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">minio&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">minio/minio&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">9000&lt;/span>:&lt;span style="color:#ae81ff">9000&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">MINIO_ACCESS_KEY=access-key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">MINIO_SECRET_KEY=secret-key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: &lt;span style="color:#ae81ff">server /export&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">createbuckets&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">minio/mc&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">depends_on&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">minio&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">entrypoint&lt;/span>: &amp;gt;&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> /bin/sh -c &amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> apk add nc &amp;amp;&amp;amp;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> while ! nc -z minio 9000; do echo &amp;#39;Wait minio to startup...&amp;#39; &amp;amp;&amp;amp; sleep 0.1; done; sleep 5 &amp;amp;&amp;amp;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> /usr/bin/mc config host add myminio http://minio:9000 access-key secret-key;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> /usr/bin/mc mb myminio/my-local-bucket;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> /usr/bin/mc policy download myminio/my-local-bucket;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> exit 0;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>app&lt;/code> is my Django app. Nothing new here.&lt;/li>
&lt;li>&lt;code>minio&lt;/code> is the MinIO instance.&lt;/li>
&lt;li>&lt;code>createbuckets&lt;/code> is a quick instance that creates a new bucket on startup, that
way we don&amp;rsquo;t need to create the bucket manually.&lt;/li>
&lt;/ul>
&lt;p>On my app, in &lt;code>settings.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># S3 configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>DEFAULT_FILE_STORAGE &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;storages.backends.s3boto3.S3Boto3Storage&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AWS_ACCESS_KEY_ID &lt;span style="color:#f92672">=&lt;/span> os&lt;span style="color:#f92672">.&lt;/span>environ&lt;span style="color:#f92672">.&lt;/span>get(&lt;span style="color:#e6db74">&amp;#34;AWS_ACCESS_KEY_ID&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;access-key&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AWS_SECRET_ACCESS_KEY &lt;span style="color:#f92672">=&lt;/span> os&lt;span style="color:#f92672">.&lt;/span>environ&lt;span style="color:#f92672">.&lt;/span>get(&lt;span style="color:#e6db74">&amp;#34;AWS_SECRET_ACCESS_KEY&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;secret-key&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AWS_STORAGE_BUCKET_NAME &lt;span style="color:#f92672">=&lt;/span> os&lt;span style="color:#f92672">.&lt;/span>environ&lt;span style="color:#f92672">.&lt;/span>get(&lt;span style="color:#e6db74">&amp;#34;AWS_STORAGE_BUCKET_NAME&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;my-local-bucket&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> DEBUG:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AWS_S3_ENDPOINT_URL &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;http://minio:9000&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If we were in a production environment, the &lt;code>AWS_ACCESS_KEY_ID&lt;/code>,
&lt;code>AWS_SECRET_ACCESS_KEY&lt;/code> and &lt;code>AWS_STORAGE_BUCKET_NAME&lt;/code> would be read from the
environmental variables, but since we haven&amp;rsquo;t set those up and we have
&lt;code>DEBUG=True&lt;/code>, we are going to use the default ones, which point directly to
MinIO.&lt;/p>
&lt;p>And that&amp;rsquo;s it! That&amp;rsquo;s everything you need to have your local S3 development environment.&lt;/p>
&lt;h2 id="testing">Testing&lt;/h2>
&lt;p>First, let&amp;rsquo;s create our model. This is a simple mock model for testing purposes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.db &lt;span style="color:#f92672">import&lt;/span> models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Person&lt;/span>(models&lt;span style="color:#f92672">.&lt;/span>Model):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;This is a demo person model&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> first_name &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>CharField(max_length&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">50&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> last_name &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>CharField(max_length&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">50&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> date_of_birth &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>DateField()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> picture &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>ImageField()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __str__(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>first_name&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>last_name&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>str(self&lt;span style="color:#f92672">.&lt;/span>date_of_birth)&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then, in the Django admin we can interact with our new model:&lt;/p>
&lt;figure>&lt;img src="https://rogs.me/2021-01-10-135111.webp"/>
&lt;/figure>
&lt;figure>&lt;img src="https://rogs.me/2021-01-10-135130.webp"/>
&lt;/figure>
&lt;p>If we go to the URL and change the domain to &lt;code>localhost&lt;/code>, we should be able to
see the picture we uploaded.&lt;/p>
&lt;figure>&lt;img src="https://rogs.me/2021-01-10-140016.webp"/>
&lt;/figure>
&lt;h2 id="bonus-the-minio-browser">Bonus: The MinIO browser&lt;/h2>
&lt;p>MinIO has a local objects browser. If you want to check it out you just need to
go to &lt;a href="http://localhost:9000">http://localhost:9000&lt;/a>. With my docker-compose configuration, the
credentials are:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>username: access-key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>password: secret-key
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;figure>&lt;img src="https://rogs.me/2021-01-10-140236.webp"/>
&lt;/figure>
&lt;p>On the browser, you can see your uploads, delete them, add new ones, etc.&lt;/p>
&lt;figure>&lt;img src="https://rogs.me/2021-01-10-140337.webp"/>
&lt;/figure>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>Now you can have a simple configuration for your local and production
environments to work seamlessly, using local resources instead of remote
resources that might generate costs for the development.&lt;/p>
&lt;p>If you want to check out the project code, you can check in my Gitlab here:
&lt;a href="https://gitlab.com/rogs/minio-example">https://gitlab.com/rogs/minio-example&lt;/a>&lt;/p>
&lt;p>See you in the next one!&lt;/p></description></item><item><title>How to create a celery task that fills out fields using Django</title><link>https://rogs.me/2020/11/how-to-create-a-celery-task-that-fills-out-fields-using-django/</link><pubDate>Sun, 29 Nov 2020 15:48:48 -0300</pubDate><guid>https://rogs.me/2020/11/how-to-create-a-celery-task-that-fills-out-fields-using-django/</guid><description>&lt;p>Hi everyone!&lt;/p>
&lt;p>It&amp;rsquo;s been way too long, I know. In this oportunity, I wanted to talk about
asynchronicity in Django, but first, lets set up the stage:&lt;/p>
&lt;p>Imagine you are working in a library and you have to develop an app that allows
users to register new books using a barcode scanner. The system has to read the
ISBN code and use an external resource to fill in the information (title, pages,
authors, etc.). You don&amp;rsquo;t need the complete book information to continue, so the
external resource can&amp;rsquo;t hold the request.&lt;/p>
&lt;p>&lt;strong>How can you process the external request asynchronously?&lt;/strong> 🤔&lt;/p>
&lt;p>For that, we need Celery.&lt;/p>
&lt;h2 id="what-is-celery">What is Celery?&lt;/h2>
&lt;p>&lt;a href="https://docs.celeryproject.org/en/stable/">Celery&lt;/a> is a &amp;ldquo;distributed task queue&amp;rdquo;. Fron their website:&lt;/p>
&lt;p>&amp;gt; Celery is a simple, flexible, and reliable distributed system to process vast
amounts of messages, while providing operations with the tools required to
maintain such a system.&lt;/p>
&lt;p>So Celery can get messages from external processes via a broker (like &lt;a href="https://redis.io/">Redis&lt;/a>),
and process them.&lt;/p>
&lt;p>The best thing is: Django can connect to Celery very easily, and Celery can
access Django models without any problem. Sweet!&lt;/p>
&lt;h2 id="lets-code">Lets code!&lt;/h2>
&lt;p>Let&amp;rsquo;s assume our project structure is the following:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-nil" data-lang="nil">- app/
- manage.py
- app/
- __init__.py
- settings.py
- urls.py
&lt;/code>&lt;/pre>&lt;h3 id="celery">Celery&lt;/h3>
&lt;p>First, we need to set up Celery in Django. Thankfully, &lt;a href="https://docs.celeryproject.org/en/stable/django/first-steps-with-django.html#using-celery-with-django">Celery has an excellent
documentation&lt;/a>, but the entire process can be summarized to this:&lt;/p>
&lt;p>In &lt;code>app/app/celery.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> os
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> celery &lt;span style="color:#f92672">import&lt;/span> Celery
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># set the default Django settings module for the &amp;#39;celery&amp;#39; program.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>os&lt;span style="color:#f92672">.&lt;/span>environ&lt;span style="color:#f92672">.&lt;/span>setdefault(&lt;span style="color:#e6db74">&amp;#34;DJANGO_SETTINGS_MODULE&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;app.settings&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app &lt;span style="color:#f92672">=&lt;/span> Celery(&lt;span style="color:#e6db74">&amp;#34;app&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Using a string here means the worker doesn&amp;#39;t have to serialize&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># the configuration object to child processes.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># - namespace=&amp;#39;CELERY&amp;#39; means all celery-related configuration keys&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># should have a `CELERY_` prefix.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app&lt;span style="color:#f92672">.&lt;/span>config_from_object(&lt;span style="color:#e6db74">&amp;#34;django.conf:settings&amp;#34;&lt;/span>, namespace&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;CELERY&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Load task modules from all registered Django app configs.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app&lt;span style="color:#f92672">.&lt;/span>autodiscover_tasks()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@app.task&lt;/span>(bind&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">debug_task&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;A debug celery task&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Request: &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>request&lt;span style="color:#e6db74">!r}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>What&amp;rsquo;s going on here?&lt;/p>
&lt;ul>
&lt;li>First, we set the &lt;code>DJANGO_SETTINGS_MODULE&lt;/code> environment variable&lt;/li>
&lt;li>Then, we instantiate our Celery app using the &lt;code>app&lt;/code> variable.&lt;/li>
&lt;li>Then, we tell Celery to look for celery configurations in the Django settings
with the &lt;code>CELERY&lt;/code> prefix. We will see this later in the post.&lt;/li>
&lt;li>Finally, we start Celery&amp;rsquo;s &lt;code>autodiscover_tasks&lt;/code>. Celery is now going to look for
&lt;code>tasks.py&lt;/code> files in the Django apps.&lt;/li>
&lt;/ul>
&lt;p>In &lt;code>/app/app/__init__.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># This will make sure the app is always imported when&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Django starts so that shared_task will use this app.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> .celery &lt;span style="color:#f92672">import&lt;/span> app &lt;span style="color:#66d9ef">as&lt;/span> celery_app
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>__all__ &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#e6db74">&amp;#34;celery_app&amp;#34;&lt;/span>,)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Finally in &lt;code>/app/app/settings.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Celery&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>CELERY_BROKER_URL &lt;span style="color:#f92672">=&lt;/span> env&lt;span style="color:#f92672">.&lt;/span>str(&lt;span style="color:#e6db74">&amp;#34;CELERY_BROKER_URL&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>CELERY_TIMEZONE &lt;span style="color:#f92672">=&lt;/span> env&lt;span style="color:#f92672">.&lt;/span>str(&lt;span style="color:#e6db74">&amp;#34;CELERY_TIMEZONE&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;America/Montevideo&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>CELERY_RESULT_BACKEND &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;django-db&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>CELERY_CACHE_BACKEND &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;django-cache&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here, we can see that the &lt;code>CELERY&lt;/code> prefix is used for all Celery configurations,
because on &lt;code>celery.py&lt;/code> we told Celery the prefix was &lt;code>CELERY&lt;/code>&lt;/p>
&lt;p>With this, Celery is fully configured. 🎉&lt;/p>
&lt;h3 id="django">Django&lt;/h3>
&lt;p>First, let&amp;rsquo;s create a &lt;code>core&lt;/code> app. This is going to be used for everything common
in the app&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ python manage.py startapp core
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>On &lt;code>core/models.py&lt;/code>, lets set the following models:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">Models
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> uuid
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.db &lt;span style="color:#f92672">import&lt;/span> models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">TimeStampMixin&lt;/span>(models&lt;span style="color:#f92672">.&lt;/span>Model):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> A base model that all the other models inherit from.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> This is to add created_at and updated_at to every model.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>UUIDField(primary_key&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>, default&lt;span style="color:#f92672">=&lt;/span>uuid&lt;span style="color:#f92672">.&lt;/span>uuid4)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> created_at &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>DateTimeField(auto_now_add&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> updated_at &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>DateTimeField(auto_now&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Meta&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Setting up the abstract model class&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> abstract &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BaseAttributesModel&lt;/span>(TimeStampMixin):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> A base model that sets up all the attibutes models
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>CharField(max_length&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">255&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> outside_url &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>URLField()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __str__(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Meta&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> abstract &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then, let&amp;rsquo;s create a new app for our books:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>python manage.py startapp books
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And on &lt;code>books/models.py&lt;/code>, let&amp;rsquo;s create the following models:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">Books models
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.db &lt;span style="color:#f92672">import&lt;/span> models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> core.models &lt;span style="color:#f92672">import&lt;/span> TimeStampMixin, BaseAttributesModel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Author&lt;/span>(BaseAttributesModel):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Defines the Author model&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">People&lt;/span>(BaseAttributesModel):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Defines the People model&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Subject&lt;/span>(BaseAttributesModel):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Defines the Subject model&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Book&lt;/span>(TimeStampMixin):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Defines the Book model&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> isbn &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>CharField(max_length&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">13&lt;/span>, unique&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> title &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>CharField(max_length&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">255&lt;/span>, blank&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>, null&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pages &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>IntegerField(default&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> publish_date &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>CharField(max_length&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">255&lt;/span>, blank&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>, null&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> outside_id &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>CharField(max_length&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">255&lt;/span>, blank&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>, null&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> outside_url &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>URLField(blank&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>, null&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> author &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>ManyToManyField(Author, related_name&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;books&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> person &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>ManyToManyField(People, related_name&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;books&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subject &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>ManyToManyField(Subject, related_name&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;books&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __str__(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>title&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> - &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>isbn&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Author&lt;/code>, &lt;code>People&lt;/code>, and &lt;code>Subject&lt;/code> are all &lt;code>BaseAttributesModel&lt;/code>, so their fields
come from the class we defined on &lt;code>core/models.py&lt;/code>.&lt;/p>
&lt;p>For &lt;code>Book&lt;/code> we add all the fields we need, plus a &lt;code>many_to_many&lt;/code> with Author,
People and Subjects. Because:&lt;/p>
&lt;ul>
&lt;li>&lt;em>Books can have many authors, and many authors can have many books&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>Example: &lt;a href="https://www.epicreads.com/blog/ya-books-multiple-authors/">27 Books by Multiple Authors That Prove the More, the Merrier&lt;/a>&lt;/p>
&lt;ul>
&lt;li>&lt;em>Books can have many persons, and many persons can have many books&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>Example: Ron Weasley is in several &lt;em>Harry Potter&lt;/em> books&lt;/p>
&lt;ul>
&lt;li>&lt;em>Books can have many subjects, and many subjects can have many books&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>Example: A book can be a &lt;em>comedy&lt;/em>, &lt;em>fiction&lt;/em>, and &lt;em>mystery&lt;/em> at the same time&lt;/p>
&lt;p>Let&amp;rsquo;s create &lt;code>books/serializers.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">Serializers for the Books
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.db.utils &lt;span style="color:#f92672">import&lt;/span> IntegrityError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> rest_framework &lt;span style="color:#f92672">import&lt;/span> serializers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> books.models &lt;span style="color:#f92672">import&lt;/span> Book, Author, People, Subject
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> books.tasks &lt;span style="color:#f92672">import&lt;/span> get_books_information
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">AuthorInBookSerializer&lt;/span>(serializers&lt;span style="color:#f92672">.&lt;/span>ModelSerializer):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Serializer for the Author objects inside Book&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Meta&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model &lt;span style="color:#f92672">=&lt;/span> Author
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fields &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#e6db74">&amp;#34;id&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">PeopleInBookSerializer&lt;/span>(serializers&lt;span style="color:#f92672">.&lt;/span>ModelSerializer):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Serializer for the People objects inside Book&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Meta&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model &lt;span style="color:#f92672">=&lt;/span> People
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fields &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#e6db74">&amp;#34;id&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">SubjectInBookSerializer&lt;/span>(serializers&lt;span style="color:#f92672">.&lt;/span>ModelSerializer):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Serializer for the Subject objects inside Book&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Meta&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model &lt;span style="color:#f92672">=&lt;/span> Subject
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fields &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#e6db74">&amp;#34;id&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BookSerializer&lt;/span>(serializers&lt;span style="color:#f92672">.&lt;/span>ModelSerializer):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Serializer for the Book objects&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> author &lt;span style="color:#f92672">=&lt;/span> AuthorInBookSerializer(many&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>, read_only&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> person &lt;span style="color:#f92672">=&lt;/span> PeopleInBookSerializer(many&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>, read_only&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subject &lt;span style="color:#f92672">=&lt;/span> SubjectInBookSerializer(many&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>, read_only&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Meta&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model &lt;span style="color:#f92672">=&lt;/span> Book
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fields &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;__all__&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BulkBookSerializer&lt;/span>(serializers&lt;span style="color:#f92672">.&lt;/span>Serializer):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Serializer for bulk book creating&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> isbn &lt;span style="color:#f92672">=&lt;/span> serializers&lt;span style="color:#f92672">.&lt;/span>ListField()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">create&lt;/span>(self, validated_data):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return_dict &lt;span style="color:#f92672">=&lt;/span> {&lt;span style="color:#e6db74">&amp;#34;isbn&amp;#34;&lt;/span>: []}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> isbn &lt;span style="color:#f92672">in&lt;/span> validated_data[&lt;span style="color:#e6db74">&amp;#34;isbn&amp;#34;&lt;/span>]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Book&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>create(isbn&lt;span style="color:#f92672">=&lt;/span>isbn)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return_dict[&lt;span style="color:#e6db74">&amp;#34;isbn&amp;#34;&lt;/span>]&lt;span style="color:#f92672">.&lt;/span>append(isbn)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">except&lt;/span> IntegrityError &lt;span style="color:#66d9ef">as&lt;/span> error:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">pass&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> return_dict
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">update&lt;/span>(self, instance, validated_data):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;The update method needs to be overwritten on
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> serializers.Serializer. Since we don&amp;#39;t need it, let&amp;#39;s just
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> pass it&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">pass&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BaseAttributesSerializer&lt;/span>(serializers&lt;span style="color:#f92672">.&lt;/span>ModelSerializer):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;A base serializer for the attributes objects&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> books &lt;span style="color:#f92672">=&lt;/span> BookSerializer(many&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>, read_only&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">AuthorSerializer&lt;/span>(BaseAttributesSerializer):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Serializer for the Author objects&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Meta&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model &lt;span style="color:#f92672">=&lt;/span> Author
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fields &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#e6db74">&amp;#34;id&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;outside_url&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;books&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">PeopleSerializer&lt;/span>(BaseAttributesSerializer):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Serializer for the Author objects&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Meta&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model &lt;span style="color:#f92672">=&lt;/span> People
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fields &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#e6db74">&amp;#34;id&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;outside_url&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;books&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">SubjectSerializer&lt;/span>(BaseAttributesSerializer):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Serializer for the Author objects&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Meta&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model &lt;span style="color:#f92672">=&lt;/span> Subject
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fields &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#e6db74">&amp;#34;id&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;outside_url&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;books&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The most important serializer here is &lt;code>BulkBookSerializer&lt;/code>. It&amp;rsquo;s going to get an
ISBN list and then bulk create them in the DB.&lt;/p>
&lt;p>On &lt;code>books/views.py&lt;/code>, we can set the following views:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">Views for the Books
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> rest_framework &lt;span style="color:#f92672">import&lt;/span> viewsets, mixins, generics
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> rest_framework.permissions &lt;span style="color:#f92672">import&lt;/span> AllowAny
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> books.models &lt;span style="color:#f92672">import&lt;/span> Book, Author, People, Subject
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> books.serializers &lt;span style="color:#f92672">import&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BookSerializer,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BulkBookSerializer,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthorSerializer,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PeopleSerializer,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SubjectSerializer,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BookViewSet&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> viewsets&lt;span style="color:#f92672">.&lt;/span>GenericViewSet,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mixins&lt;span style="color:#f92672">.&lt;/span>ListModelMixin,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mixins&lt;span style="color:#f92672">.&lt;/span>RetrieveModelMixin,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> A view to list Books and retrieve books by ID
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> permission_classes &lt;span style="color:#f92672">=&lt;/span> (AllowAny,)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> queryset &lt;span style="color:#f92672">=&lt;/span> Book&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>all()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> serializer_class &lt;span style="color:#f92672">=&lt;/span> BookSerializer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">AuthorViewSet&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> viewsets&lt;span style="color:#f92672">.&lt;/span>GenericViewSet,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mixins&lt;span style="color:#f92672">.&lt;/span>ListModelMixin,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mixins&lt;span style="color:#f92672">.&lt;/span>RetrieveModelMixin,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> A view to list Authors and retrieve authors by ID
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> permission_classes &lt;span style="color:#f92672">=&lt;/span> (AllowAny,)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> queryset &lt;span style="color:#f92672">=&lt;/span> Author&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>all()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> serializer_class &lt;span style="color:#f92672">=&lt;/span> AuthorSerializer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">PeopleViewSet&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> viewsets&lt;span style="color:#f92672">.&lt;/span>GenericViewSet,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mixins&lt;span style="color:#f92672">.&lt;/span>ListModelMixin,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mixins&lt;span style="color:#f92672">.&lt;/span>RetrieveModelMixin,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> A view to list People and retrieve people by ID
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> permission_classes &lt;span style="color:#f92672">=&lt;/span> (AllowAny,)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> queryset &lt;span style="color:#f92672">=&lt;/span> People&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>all()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> serializer_class &lt;span style="color:#f92672">=&lt;/span> PeopleSerializer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">SubjectViewSet&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> viewsets&lt;span style="color:#f92672">.&lt;/span>GenericViewSet,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mixins&lt;span style="color:#f92672">.&lt;/span>ListModelMixin,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mixins&lt;span style="color:#f92672">.&lt;/span>RetrieveModelMixin,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> A view to list Subject and retrieve subject by ID
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> permission_classes &lt;span style="color:#f92672">=&lt;/span> (AllowAny,)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> queryset &lt;span style="color:#f92672">=&lt;/span> Subject&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>all()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> serializer_class &lt;span style="color:#f92672">=&lt;/span> SubjectSerializer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BulkCreateBook&lt;/span>(generics&lt;span style="color:#f92672">.&lt;/span>CreateAPIView):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;A view to bulk create books&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> permission_classes &lt;span style="color:#f92672">=&lt;/span> (AllowAny,)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> queryset &lt;span style="color:#f92672">=&lt;/span> Book&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>all()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> serializer_class &lt;span style="color:#f92672">=&lt;/span> BulkBookSerializer
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Easy enough, endpoints for getting books, authors, people and subjects and an
endpoint to post ISBN codes in a list.&lt;/p>
&lt;p>We can check swagger to see all the endpoints created:&lt;/p>
&lt;figure>&lt;img src="https://rogs.me/2020-11-29-115634.webp"/>
&lt;/figure>
&lt;p>Now, &lt;strong>how are we going to get all the data?&lt;/strong> 🤔&lt;/p>
&lt;h2 id="creating-a-celery-task">Creating a Celery task&lt;/h2>
&lt;p>Now that we have our project structure done, we need to create the asynchronous
task Celery is going to run to populate our fields.&lt;/p>
&lt;p>To get the information, we are going to use the &lt;a href="https://openlibrary.org/dev/docs/api/books%22%22%22">OpenLibrary API&lt;/a>.&lt;/p>
&lt;p>First, we need to create &lt;code>books/tasks.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">Celery tasks
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> requests
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> celery &lt;span style="color:#f92672">import&lt;/span> shared_task
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> books.models &lt;span style="color:#f92672">import&lt;/span> Book, Author, People, Subject
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">get_book_info&lt;/span>(isbn):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Gets a book information by using its ISBN.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> More info here https://openlibrary.org/dev/docs/api/books&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> requests&lt;span style="color:#f92672">.&lt;/span>get(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://openlibrary.org/api/books?jscmd=data&amp;amp;format=json&amp;amp;bibkeys=ISBN:&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>isbn&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )&lt;span style="color:#f92672">.&lt;/span>json()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">generate_many_to_many&lt;/span>(model, iterable):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Generates the many to many relationships to books&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return_items &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> item &lt;span style="color:#f92672">in&lt;/span> iterable:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> relation &lt;span style="color:#f92672">=&lt;/span> model&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>get_or_create(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name&lt;span style="color:#f92672">=&lt;/span>item[&lt;span style="color:#e6db74">&amp;#34;name&amp;#34;&lt;/span>], outside_url&lt;span style="color:#f92672">=&lt;/span>item[&lt;span style="color:#e6db74">&amp;#34;url&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return_items&lt;span style="color:#f92672">.&lt;/span>append(relation)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> return_items
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@shared_task&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">get_books_information&lt;/span>(isbn):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Gets a book information&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># First, we get the book information by its isbn&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book_info &lt;span style="color:#f92672">=&lt;/span> get_book_info(isbn)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> len(book_info) &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Then, we need to access the json itself. Since the first key is dynamic,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># we get it by accessing the json keys&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> key &lt;span style="color:#f92672">=&lt;/span> list(book_info&lt;span style="color:#f92672">.&lt;/span>keys())[&lt;span style="color:#ae81ff">0&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book_info &lt;span style="color:#f92672">=&lt;/span> book_info[key]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Since the book was created on the Serializer, we get the book to edit&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book &lt;span style="color:#f92672">=&lt;/span> Book&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>get(isbn&lt;span style="color:#f92672">=&lt;/span>isbn)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Set the fields we want from the API into the Book&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book&lt;span style="color:#f92672">.&lt;/span>title &lt;span style="color:#f92672">=&lt;/span> book_info[&lt;span style="color:#e6db74">&amp;#34;title&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book&lt;span style="color:#f92672">.&lt;/span>publish_date &lt;span style="color:#f92672">=&lt;/span> book_info[&lt;span style="color:#e6db74">&amp;#34;publish_date&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book&lt;span style="color:#f92672">.&lt;/span>outside_id &lt;span style="color:#f92672">=&lt;/span> book_info[&lt;span style="color:#e6db74">&amp;#34;key&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book&lt;span style="color:#f92672">.&lt;/span>outside_url &lt;span style="color:#f92672">=&lt;/span> book_info[&lt;span style="color:#e6db74">&amp;#34;url&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># For the optional fields, we try to get them first&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book&lt;span style="color:#f92672">.&lt;/span>pages &lt;span style="color:#f92672">=&lt;/span> book_info[&lt;span style="color:#e6db74">&amp;#34;number_of_pages&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">except&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book&lt;span style="color:#f92672">.&lt;/span>pages &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> authors &lt;span style="color:#f92672">=&lt;/span> book_info[&lt;span style="color:#e6db74">&amp;#34;authors&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">except&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> authors &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> people &lt;span style="color:#f92672">=&lt;/span> book_info[&lt;span style="color:#e6db74">&amp;#34;subject_people&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">except&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> people &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subjects &lt;span style="color:#f92672">=&lt;/span> book_info[&lt;span style="color:#e6db74">&amp;#34;subjects&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">except&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subjects &lt;span style="color:#f92672">=&lt;/span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># And generate the appropiate many_to_many relationships&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> authors_info &lt;span style="color:#f92672">=&lt;/span> generate_many_to_many(Author, authors)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> people_info &lt;span style="color:#f92672">=&lt;/span> generate_many_to_many(People, people)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subjects_info &lt;span style="color:#f92672">=&lt;/span> generate_many_to_many(Subject, subjects)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Once the relationships are generated, we save them in the book instance&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> author &lt;span style="color:#f92672">in&lt;/span> authors_info:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book&lt;span style="color:#f92672">.&lt;/span>author&lt;span style="color:#f92672">.&lt;/span>add(author[&lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> person &lt;span style="color:#f92672">in&lt;/span> people_info:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book&lt;span style="color:#f92672">.&lt;/span>person&lt;span style="color:#f92672">.&lt;/span>add(person[&lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> subject &lt;span style="color:#f92672">in&lt;/span> subjects_info:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book&lt;span style="color:#f92672">.&lt;/span>subject&lt;span style="color:#f92672">.&lt;/span>add(subject[&lt;span style="color:#ae81ff">0&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Finally, we save the Book&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> book&lt;span style="color:#f92672">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">raise&lt;/span> &lt;span style="color:#a6e22e">ValueError&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;Book not found&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>So when are we going to run this task? We need to run it in the &lt;strong>serializer&lt;/strong>.&lt;/p>
&lt;p>On &lt;code>books/serializers.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> books.tasks &lt;span style="color:#f92672">import&lt;/span> get_books_information
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BulkBookSerializer&lt;/span>(serializers&lt;span style="color:#f92672">.&lt;/span>Serializer):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Serializer for bulk book creating&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> isbn &lt;span style="color:#f92672">=&lt;/span> serializers&lt;span style="color:#f92672">.&lt;/span>ListField()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">create&lt;/span>(self, validated_data):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return_dict &lt;span style="color:#f92672">=&lt;/span> {&lt;span style="color:#e6db74">&amp;#34;isbn&amp;#34;&lt;/span>: []}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> isbn &lt;span style="color:#f92672">in&lt;/span> validated_data[&lt;span style="color:#e6db74">&amp;#34;isbn&amp;#34;&lt;/span>]:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Book&lt;span style="color:#f92672">.&lt;/span>objects&lt;span style="color:#f92672">.&lt;/span>create(isbn&lt;span style="color:#f92672">=&lt;/span>isbn)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># We need to add this line&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> get_books_information&lt;span style="color:#f92672">.&lt;/span>delay(isbn)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">#################################&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return_dict[&lt;span style="color:#e6db74">&amp;#34;isbn&amp;#34;&lt;/span>]&lt;span style="color:#f92672">.&lt;/span>append(isbn)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">except&lt;/span> IntegrityError &lt;span style="color:#66d9ef">as&lt;/span> error:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">pass&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> return_dict
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">update&lt;/span>(self, instance, validated_data):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">pass&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To trigger the Celery tasks, we need to call our function with the &lt;code>delay&lt;/code>
function, which has been added by the &lt;code>shared_task&lt;/code> decorator. This tells Celery
to start running the task in the background since we don&amp;rsquo;t need the result
right now.&lt;/p>
&lt;h2 id="docker-configuration">Docker configuration&lt;/h2>
&lt;p>There are a lot of moving parts we need for this to work, so I created a
&lt;code>docker-compose&lt;/code> configuration to help with the stack. I&amp;rsquo;m using the package
&lt;a href="https://github.com/joke2k/django-environ">django-environ&lt;/a> to handle all environment variables.&lt;/p>
&lt;p>On &lt;code>docker-compose.yml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;3.7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">x-common-variables&lt;/span>: &lt;span style="color:#75715e">&amp;amp;common-variables&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">DJANGO_SETTINGS_MODULE&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;app.settings&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">CELERY_BROKER_URL&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;redis://redis:6379&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">DEFAULT_DATABASE&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;psql://postgres:postgres@db:5432/app&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">DEBUG&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;True&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ALLOWED_HOSTS&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;*,test&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">SECRET_KEY&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;this-is-a-secret-key-shhhhh&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">app&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">build&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">context&lt;/span>: &lt;span style="color:#ae81ff">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">./app:/app&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span>: &lt;span style="color:#75715e">*common-variables&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">8000&lt;/span>:&lt;span style="color:#ae81ff">8000&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: &amp;gt;&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> sh -c &amp;#34;python manage.py migrate &amp;amp;&amp;amp;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> python manage.py runserver 0.0.0.0:8000&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">depends_on&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">db&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">redis&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">celery-worker&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">build&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">context&lt;/span>: &lt;span style="color:#ae81ff">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">./app:/app&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span>: &lt;span style="color:#75715e">*common-variables&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: &lt;span style="color:#ae81ff">celery --app app worker -l info&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">depends_on&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">db&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">redis&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">db&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">postgres:12.4-alpine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">POSTGRES_DB=app&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">POSRGRES_USER=postgres&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">POSTGRES_PASSWORD=postgres&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">redis&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">redis:6.0.8-alpine&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is going to set our app, DB, Redis, and most importantly our celery-worker
instance. To run Celery, we need to execute:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ celery --app app worker -l info
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>So we are going to run that command on a separate docker instance&lt;/p>
&lt;h2 id="testing-it-out">Testing it out&lt;/h2>
&lt;p>If we run&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ docker-compose up
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>on our project root folder, the project should come up as usual. You should be
able to open &lt;a href="http://localhost:8000/admin">http://localhost:8000/admin&lt;/a> and enter the admin panel.&lt;/p>
&lt;p>To test the app, you can use a curl command from the terminal:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>curl -X POST &lt;span style="color:#e6db74">&amp;#34;http://localhost:8000/books/bulk-create&amp;#34;&lt;/span> -H &lt;span style="color:#e6db74">&amp;#34;accept: application/json&amp;#34;&lt;/span> &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -H &lt;span style="color:#e6db74">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> -d &lt;span style="color:#e6db74">&amp;#34;{ \&amp;#34;isbn\&amp;#34;: [ \&amp;#34;9780345418913\&amp;#34;, \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> \&amp;#34;9780451524935\&amp;#34;, \&amp;#34;9780451526342\&amp;#34;, \&amp;#34;9781101990322\&amp;#34;, \&amp;#34;9780143133438\&amp;#34; ]}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;figure>&lt;img src="https://rogs.me/2020-11-29-124654.webp"/>
&lt;/figure>
&lt;p>This call lasted 147ms, according to my terminal.&lt;/p>
&lt;p>This should return instantly, creating 15 new books and 15 new Celery tasks, one
for each book. You can also see tasks results in the Django admin using the
&lt;code>django-celery-results&lt;/code> package, check its &lt;a href="https://docs.celeryproject.org/en/stable/django/first-steps-with-django.html#django-celery-results-using-the-django-orm-cache-as-a-result-backend">documentation&lt;/a>.&lt;/p>
&lt;figure>&lt;img src="https://rogs.me/2020-11-29-124734.webp"/>
&lt;/figure>
&lt;p>Celery tasks list, using &lt;code>django-celery-results&lt;/code>&lt;/p>
&lt;figure>&lt;img src="https://rogs.me/2020-11-29-124751.webp"/>
&lt;/figure>
&lt;p>Created and processed books list&lt;/p>
&lt;figure>&lt;img src="https://rogs.me/2020-11-29-124813.webp"/>
&lt;/figure>
&lt;p>Single book information&lt;/p>
&lt;figure>&lt;img src="https://rogs.me/2020-11-29-124834.webp"/>
&lt;/figure>
&lt;p>People in books&lt;/p>
&lt;figure>&lt;img src="https://rogs.me/2020-11-29-124851.webp"/>
&lt;/figure>
&lt;p>Authors&lt;/p>
&lt;figure>&lt;img src="https://rogs.me/2020-11-29-124906.webp"/>
&lt;/figure>
&lt;p>Themes&lt;/p>
&lt;p>And also, you can interact with the endpoints to search by author, theme,
people, and book. This should change depending on how you created your URLs.&lt;/p>
&lt;h2 id="that-s-it">That&amp;rsquo;s it!&lt;/h2>
&lt;p>This surely was a &lt;strong>LONG&lt;/strong> one, but it has been a very good one in my opinion.
I&amp;rsquo;ve used Celery in the past for multiple things, from sending emails in the
background to triggering scraping jobs and &lt;a href="https://docs.celeryproject.org/en/stable/userguide/periodic-tasks.html#using-custom-scheduler-classes">running scheduled tasks&lt;/a> (like a &lt;a href="https://en.wikipedia.org/wiki/Cron">unix
cronjob&lt;/a>)&lt;/p>
&lt;p>You can check the complete project in my GitLab here: &lt;a href="https://gitlab.com/rogs/books-app">https://gitlab.com/rogs/books-app&lt;/a>&lt;/p>
&lt;p>If you have any doubts, let me know! I always answer emails and/or messages.&lt;/p></description></item><item><title>How I got a residency appointment thanks to Python, Selenium and Telegram</title><link>https://rogs.me/2020/08/how-i-got-a-residency-appointment-thanks-to-python-selenium-and-telegram/</link><pubDate>Sun, 02 Aug 2020 00:00:00 +0000</pubDate><guid>https://rogs.me/2020/08/how-i-got-a-residency-appointment-thanks-to-python-selenium-and-telegram/</guid><description>&lt;p>Hello everyone&lt;/p>
&lt;p>As some of you might know, I&amp;rsquo;m a Venezuelan 🇻🇪 living in Montevideo, Uruguay 🇺🇾.
I&amp;rsquo;ve been living here for almost a year, but because of the pandemic my
residency appointments have slowed down to a crawl, and in the middle of the
quarantine they added a new appointment system. Before, there were no
appointments, you just had to get there early and wait for the secretary to
review your files and assign someone to attend you. But now, they had
implemented an appointment system that you could do from the comfort of your own
home/office. There was just one issue: &lt;strong>there were never appointments available&lt;/strong>.&lt;/p>
&lt;p>That was a little stressful. I was developing a small &lt;em>tick&lt;/em> by checking the
site multiple times a day, with no luck. But then, I decided I wanted to do a
bot that checks the site for me, that way I could just forget about it and let
the computers do it for me.&lt;/p>
&lt;h2 id="tech">Tech&lt;/h2>
&lt;h3 id="selenium">Selenium&lt;/h3>
&lt;p>I had some experience with Selenium in the past because I had to run automated
tests on an Android application, but I had never used it for the web. I knew it
supported Firefox and had an extensive API to interact with websites. In the
end, I just had to inspect the HTML and search for the &amp;ldquo;No appointments
available&amp;rdquo; error message. If the message wasn&amp;rsquo;t there, I needed a way to be
notified so I can set my appointment as fast as possible.&lt;/p>
&lt;h3 id="telegram-bot-api">Telegram Bot API&lt;/h3>
&lt;p>Telegram was my goto because I have a lot of experience with it. It has a
stupidly easy API that allows for superb bot management. I just needed the bot
to send me a message whenever the &amp;ldquo;No appointments available&amp;rdquo; message wasn&amp;rsquo;t
found on the site.&lt;/p>
&lt;h2 id="the-plan">The plan&lt;/h2>
&lt;p>Here comes the juicy part: How is everything going to work together?&lt;/p>
&lt;p>I divided the work into four parts:&lt;/p>
&lt;ol>
&lt;li>Inspecting the site&lt;/li>
&lt;li>Finding the error message on the site&lt;/li>
&lt;li>Sending the message if nothing was found&lt;/li>
&lt;li>Deploy the job with a cronjob on my VPS&lt;/li>
&lt;/ol>
&lt;h2 id="inspecting-the-site">Inspecting the site&lt;/h2>
&lt;p>Here is the site I needed to inspect:&lt;/p>
&lt;ul>
&lt;li>On the first site, I need to click the bottom button. By inspecting the HTML,
I found out that its name is &lt;code>form:botonElegirHora&lt;/code>
&lt;img src="https://rogs.me/2020-08-02-171251.webp" alt="">&lt;/li>
&lt;li>When the button is clicked, it loads a second page that has an error message
if no appointments are found. The ID of that message is &lt;code>form:warnSinCupos&lt;/code>.
&lt;img src="https://rogs.me/2020-08-02-162205.webp" alt="">&lt;/li>
&lt;/ul>
&lt;h2 id="using-selenium-to-find-the-error-message">Using Selenium to find the error message&lt;/h2>
&lt;p>First, I needed to define the browser session and its settings. I wanted to run
it in headless mode so no X session is needed:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> selenium &lt;span style="color:#f92672">import&lt;/span> webdriver
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> selenium.webdriver.firefox.options &lt;span style="color:#f92672">import&lt;/span> Options
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>options &lt;span style="color:#f92672">=&lt;/span> Options()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>options&lt;span style="color:#f92672">.&lt;/span>headless &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>d &lt;span style="color:#f92672">=&lt;/span> webdriver&lt;span style="color:#f92672">.&lt;/span>Firefox(options&lt;span style="color:#f92672">=&lt;/span>options)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then, I opened the site, looked for the button (&lt;code>form:botonElegirHora&lt;/code>) and
clicked it&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># This is the website I wanted to scrape&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>d&lt;span style="color:#f92672">.&lt;/span>get(&lt;span style="color:#e6db74">&amp;#39;https://sae.mec.gub.uy/sae/agendarReserva/Paso1.xhtml?e=9&amp;amp;a=7&amp;amp;r=13&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>elem &lt;span style="color:#f92672">=&lt;/span> d&lt;span style="color:#f92672">.&lt;/span>find_element_by_name(&lt;span style="color:#e6db74">&amp;#39;form:botonElegirHora&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>elem&lt;span style="color:#f92672">.&lt;/span>click()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And on the new page, I looked for the error message (&lt;code>form:warnSinCupos&lt;/code>)&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> warning_message &lt;span style="color:#f92672">=&lt;/span> d&lt;span style="color:#f92672">.&lt;/span>find_element_by_id(&lt;span style="color:#e6db74">&amp;#39;form:warnSinCupos&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">except&lt;/span> &lt;span style="color:#a6e22e">Exception&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">pass&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This was working exactly how I wanted: It opened a new browser session, opened
the site, clicked the button, and then looked for the message. For now, if the
message wasn&amp;rsquo;t found, it does nothing. Now, the script needs to send me a
message if the warning message wasn&amp;rsquo;t found on the page.&lt;/p>
&lt;h2 id="using-telegram-to-send-a-message-if-the-warning-message-wasn-t-found">Using Telegram to send a message if the warning message wasn&amp;rsquo;t found&lt;/h2>
&lt;p>The Telegram bot API has a very simple way to send messages. If you want to read
more about their API, you can check it &lt;a href="https://core.telegram.org/">here&lt;/a>.&lt;/p>
&lt;p>There are a few steps you need to follow to get a Telegram bot:&lt;/p>
&lt;ol>
&lt;li>First, you need to &amp;ldquo;talk&amp;rdquo; to the &lt;a href="https://core.telegram.org/bots#6-botfather">Botfather&lt;/a> to create the bot.&lt;/li>
&lt;li>Then, you need to find your Telegram Chat ID. There are a few bots that can help
you with that, I personally use &lt;code>@get_id_bot&lt;/code>.&lt;/li>
&lt;li>Once you have the ID, you should read the &lt;code>sendMessage&lt;/code> API, since that&amp;rsquo;s the
only one we need now. You can check it &lt;a href="https://core.telegram.org/bots/api#sendmessage">here&lt;/a>.&lt;/li>
&lt;/ol>
&lt;p>So, by using the Telegram documentation, I came up with the following code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> requests
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>chat_id &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#75715e"># Insert your chat ID here&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>telegram_bot_id &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#75715e"># Insert your Telegram bot ID here&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>telegram_data &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;chat_id&amp;#34;&lt;/span>: chat_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;parse_mode&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;HTML&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;text&amp;#34;&lt;/span>: (&lt;span style="color:#e6db74">&amp;#34;&amp;lt;b&amp;gt;Hay citas!&amp;lt;/b&amp;gt;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">Hay citas en el registro civil, para &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;entrar ve a &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>SAE_URL&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>requests&lt;span style="color:#f92672">.&lt;/span>post(&lt;span style="color:#e6db74">&amp;#39;https://api.telegram.org/bot&lt;/span>&lt;span style="color:#e6db74">{telegram_bot_id}&lt;/span>&lt;span style="color:#e6db74">/sendmessage&amp;#39;&lt;/span>, data&lt;span style="color:#f92672">=&lt;/span>telegram_data)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="the-complete-script">The complete script&lt;/h2>
&lt;p>I added a few loggers and environment variables and voilá! Here is the complete code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/usr/bin/env python3&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> os
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> requests
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> datetime &lt;span style="color:#f92672">import&lt;/span> datetime
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> selenium &lt;span style="color:#f92672">import&lt;/span> webdriver
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> selenium.webdriver.firefox.options &lt;span style="color:#f92672">import&lt;/span> Options
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> dotenv &lt;span style="color:#f92672">import&lt;/span> load_dotenv
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>load_dotenv() &lt;span style="color:#75715e"># This loads the environmental variables from the .env file in the root folder&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TELEGRAM_BOT_ID &lt;span style="color:#f92672">=&lt;/span> os&lt;span style="color:#f92672">.&lt;/span>environ&lt;span style="color:#f92672">.&lt;/span>get(&lt;span style="color:#e6db74">&amp;#39;TELEGRAM_BOT_ID&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TELEGRAM_CHAT_ID &lt;span style="color:#f92672">=&lt;/span> os&lt;span style="color:#f92672">.&lt;/span>environ&lt;span style="color:#f92672">.&lt;/span>get(&lt;span style="color:#e6db74">&amp;#39;TELEGRAM_CHAT_ID&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>SAE_URL &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;https://sae.mec.gub.uy/sae/agendarReserva/Paso1.xhtml?e=9&amp;amp;a=7&amp;amp;r=13&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>options &lt;span style="color:#f92672">=&lt;/span> Options()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>options&lt;span style="color:#f92672">.&lt;/span>headless &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>d &lt;span style="color:#f92672">=&lt;/span> webdriver&lt;span style="color:#f92672">.&lt;/span>Firefox(options&lt;span style="color:#f92672">=&lt;/span>options)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>d&lt;span style="color:#f92672">.&lt;/span>get(SAE_URL)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>print(&lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#39;Headless Firefox Initialized &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>datetime&lt;span style="color:#f92672">.&lt;/span>now()&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>elem &lt;span style="color:#f92672">=&lt;/span> d&lt;span style="color:#f92672">.&lt;/span>find_element_by_name(&lt;span style="color:#e6db74">&amp;#39;form:botonElegirHora&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>elem&lt;span style="color:#f92672">.&lt;/span>click()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> warning_message &lt;span style="color:#f92672">=&lt;/span> d&lt;span style="color:#f92672">.&lt;/span>find_element_by_id(&lt;span style="color:#e6db74">&amp;#39;form:warnSinCupos&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#39;No dates yet&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#39;------------------------------&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">except&lt;/span> &lt;span style="color:#a6e22e">Exception&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> telegram_data &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;chat_id&amp;#34;&lt;/span>: TELEGRAM_CHAT_ID,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;parse_mode&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;HTML&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;text&amp;#34;&lt;/span>: (&lt;span style="color:#e6db74">&amp;#34;&amp;lt;b&amp;gt;Hay citas!&amp;lt;/b&amp;gt;&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">Hay citas en el registro civil, para &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#34;entrar ve a &lt;/span>&lt;span style="color:#e6db74">{&lt;/span>SAE_URL&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> requests&lt;span style="color:#f92672">.&lt;/span>post(&lt;span style="color:#e6db74">&amp;#39;https://api.telegram.org/bot&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">f&lt;/span>&lt;span style="color:#e6db74">&amp;#39;&lt;/span>&lt;span style="color:#e6db74">{&lt;/span>TELEGRAM_BOT_ID&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">/sendmessage&amp;#39;&lt;/span>, data&lt;span style="color:#f92672">=&lt;/span>telegram_data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#39;Dates found!&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>d&lt;span style="color:#f92672">.&lt;/span>close() &lt;span style="color:#75715e"># To close the browser connection&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Only one more thing to do, to deploy everything to my VPS&lt;/p>
&lt;h2 id="deploy-and-testing-on-the-vps">Deploy and testing on the VPS&lt;/h2>
&lt;p>This was very easy. I just needed to pull my git repo, install the
&lt;code>requirements.txt&lt;/code> and set a new cron to run every 10 minutes and check the
site. The cron settings I used where:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>*/10 * * * * /usr/bin/python3 /my/script/location/registro-civil-scraper/app.py &amp;gt;&amp;gt; /my/script/location/registro-civil-scraper/log.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>&amp;gt;&amp;gt; /my/script/location/registro-civil-scraper/log.txt&lt;/code> part is to keep the logs on a new file.&lt;/p>
&lt;h2 id="did-it-work">Did it work?&lt;/h2>
&lt;p>Yes! And it worked perfectly. I got a message the following day at 21:00
(weirdly enough, that&amp;rsquo;s 0:00GMT, so maybe they have their servers at GMT time
and it opens new appointments at 0:00).
&lt;img src="https://rogs.me/2020-08-02-170458.webp" alt="">&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>I always loved to use programming to solve simple problems. With this script, I
didn&amp;rsquo;t need to check the site every couple of hours to get an appointment, and
sincerely, I wasn&amp;rsquo;t going to check past 19:00, so I would&amp;rsquo;ve never found it by
my own.&lt;/p>
&lt;p>My brother is having similar issues in Argentina, and when I showed him this, he
said one of the funniest phrases I&amp;rsquo;ve heard about my profession:&lt;/p>
&lt;p>&amp;gt; &lt;em>&amp;ldquo;Programmers could take over the world, but they are too lazy&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>I lol&amp;rsquo;d way too hard at that.&lt;/p>
&lt;p>I loved Selenium and how it worked. Recently I created a crawler using Selenium,
Redis, peewee, and Postgres, so stay tuned if you want to know more about that.&lt;/p>
&lt;p>In the meantime, if you want to check the complete script, you can see it on my
Gitlab: &lt;a href="https://gitlab.com/rogs/registro-civil-scraper">https://gitlab.com/rogs/registro-civil-scraper&lt;/a>&lt;/p></description></item><item><title>How to search Google without using Google, the self-hosted way</title><link>https://rogs.me/2020/06/how-to-search-google-without-using-google-the-self-hosted-way/</link><pubDate>Mon, 22 Jun 2020 10:07:12 -0300</pubDate><guid>https://rogs.me/2020/06/how-to-search-google-without-using-google-the-self-hosted-way/</guid><description>&lt;p>Hello everyone!&lt;/p>
&lt;p>Last week I was talking with a friend and he was complaining about how Google knows everything about us,
so I took the chance to recommend some degoogled alternatives: I sent him my blog, recommended DuckDuckGo,
Nextcloud, Protonmail, etc. He really liked my suggestions and promised to try DuckDuckGo. A couple of
days later he came to me a little defeated, because he didn&amp;rsquo;t like the search results on DuckDuckGo and
felt bad going back to Google.&lt;/p>
&lt;p>So that got me thinking: &lt;strong>Are there some degoogled search engines that use Google as the backend but
respect our privacy?&lt;/strong>&lt;/p>
&lt;p>So I went looking and found a couple of really interesting options.&lt;/p>
&lt;h1 id="startpagecomhttpsstartpagecom">&lt;a href="https://startpage.com/">Startpage.com&lt;/a>&lt;/h1>
&lt;p>According to their &lt;a href="https://en.wikipedia.org/wiki/Startpage.com">Wikipedia&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>Startpage is a web search engine that highlights privacy as its distinguishing feature. Previously, it
was known as the metasearch engine Ixquick&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&amp;hellip;&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>On 7 July 2009, Ixquick launched Startpage.com to offer its service at a URL that is both easier to
remember and spell. In contrast to Ixquick.eu, &lt;strong>Startpage.com fetches results from the Google search
engine&lt;/strong>. This is done &lt;strong>without saving user IP addresses or giving any personal user information to
Google&amp;rsquo;s servers&lt;/strong>.&lt;/p>
&lt;/blockquote>
&lt;p>and their own &lt;a href="https://startpage.com/">website&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>You can’t beat Google when it comes to online search. So we’re paying them to use their brilliant
search results in order to remove all trackers and logs. The result: The world’s best and most private
search engine. Only now you can search without ads following you around, recommending products you’ve
already bought. And no more data mining by companies with dubious intentions. We want you to dance like
nobody’s watching and search like nobody’s watching.&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://rogs.me/2020-06-18-110253.webp" alt="2020-06-18-110253">&lt;/p>
&lt;p>So this was a good solution for my friend: He could still keep his Google search results while using a
search engine that respects his privacy&lt;/p>
&lt;p>But that wasn&amp;rsquo;t enough for me. You know I love self-hosting, so I wanted to find a solution I could run
inside my own server because that&amp;rsquo;s the only way I can be 100% sure that my searches are private and no
logs are kept on my searches, So I went to my second option: Searx&lt;/p>
&lt;h1 id="searxhttpssearxme">&lt;a href="https://searx.me/">Searx&lt;/a>&lt;/h1>
&lt;p>According to their &lt;a href="https://en.wikipedia.org/wiki/Searx">Wikipedia&lt;/a>&lt;/p>
&lt;blockquote>
&lt;p>searx (/sɜːrks/) is a free metasearch engine, available under the GNU Affero General Public License
version 3, with the aim of protecting the privacy of its users. To this end, searx does not share
users&amp;rsquo; IP addresses or search history with the search engines from which it gathers results. Tracking
cookies served by the search engines are blocked, preventing user-profiling-based results modification.
By default, searx queries are submitted via HTTP POST, to prevent users&amp;rsquo; query keywords from appearing
in webserver logs. searx was inspired by the Seeks project, though it does not implement Seeks'
peer-to-peer user-sourced results ranking.&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&amp;hellip;&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>Any user &lt;strong>may run their own instance of searx, which can be done to maximize privacy&lt;/strong>, to avoid
congestion on public instances, to preserve customized settings even if browser cookies are cleared, to
allow auditing of the source code being run, etc.&lt;/p>
&lt;/blockquote>
&lt;p>And that&amp;rsquo;s what I wanted: To host my own Searx instance on my server. And nicely enough, they supported
&lt;a href="https://github.com/asciimoo/searx#installation">Docker out of the box&lt;/a> :)&lt;/p>
&lt;p>So I created my own &lt;code>docker-compose&lt;/code> based on their &lt;code>docker-compose&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;3.7&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">filtron&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">filtron&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">dalf/filtron&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">4040&lt;/span>:&lt;span style="color:#ae81ff">4040&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">4041&lt;/span>:&lt;span style="color:#ae81ff">4041&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: -&lt;span style="color:#ae81ff">listen 0.0.0.0:4040 -api 0.0.0.0:4041 -target 0.0.0.0:8082&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">./rules.json:/etc/filtron/rules.json:rw&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">read_only&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">cap_drop&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">ALL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">network_mode&lt;/span>: &lt;span style="color:#ae81ff">host&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">searx&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">searx&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">searx/searx:latest&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: -&lt;span style="color:#ae81ff">f&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">./searx:/etc/searx:rw&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">BIND_ADDRESS=0.0.0.0:8082&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">BASE_URL=https://myurl.com/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">MORTY_URL=https://myurl.com/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">MORTY_KEY=mysupersecretkey&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">cap_drop&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">ALL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">cap_add&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">CHOWN&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">SETGID&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">SETUID&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">DAC_OVERRIDE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">network_mode&lt;/span>: &lt;span style="color:#ae81ff">host&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">morty&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">morty&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">dalf/morty&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">3000&lt;/span>:&lt;span style="color:#ae81ff">3000&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: -&lt;span style="color:#ae81ff">listen 0.0.0.0:3000 -timeout 6 -ipv6&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">MORTY_KEY=mysupersecretkey&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">logging&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">driver&lt;/span>: &lt;span style="color:#ae81ff">none&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">read_only&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">cap_drop&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">ALL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">network_mode&lt;/span>: &lt;span style="color:#ae81ff">host&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">searx-checker&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">container_name&lt;/span>: &lt;span style="color:#ae81ff">searx-checker&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">searx/searx-checker&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: -&lt;span style="color:#ae81ff">cron -o html/data/status.json http://localhost:8082&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">searx-checker:/usr/local/searx-checker/html/data:rw&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">network_mode&lt;/span>: &lt;span style="color:#ae81ff">host&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">searx-checker&lt;/span>:
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And that was it! I had my own Searx instance, that uses Google, Bing, Yahoo, DuckDuckGo and many other
sources to search around the web.&lt;/p>
&lt;p>&lt;img src="https://rogs.me/2020-06-18-124302.webp" alt="2020-06-18-124302">
&lt;img src="https://rogs.me/2020-06-18-124707.webp" alt="2020-06-18-124707">&lt;/p>
&lt;h2 id="dont-want-to-host-your-own-instance-use-a-public-one">Don&amp;rsquo;t want to host your own instance? Use a public one!&lt;/h2>
&lt;p>Searx has a lot of public instances on their website, in case you don&amp;rsquo;t want to self-host your instance
but still want all the benefits of using Searx. You can check the list here: &lt;a href="https://searx.space/">https://searx.space/&lt;/a>&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>I really like DuckDuckGo. I think it is a very good project that takes privacy to the hands of
non-technical people, but I also know that &amp;ldquo;You can’t beat Google when it comes to online search&amp;rdquo;&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>.
It is definitely possible to get good search results using privacy oriented alternatives, and in the end,
it is a very cool and rewarding experience.&lt;/p>
&lt;p>I seriously recommend you to use &lt;a href="https://startpage.com">https://startpage.com&lt;/a>, one of the instances listed in
&lt;a href="https://searx.space">https://searx.space&lt;/a>, or better yet, if you have the knowledge and the resources, to self-host your own
Searx instance and start searching the web without a big corporation watching every move you make.&lt;/p>
&lt;p>Stay private.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Taken from the Startpage website&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>De-Google my blog - How to blog in 2020 without Google</title><link>https://rogs.me/2020/05/de-google-my-blog-how-to-blog-in-2020-without-google/</link><pubDate>Wed, 20 May 2020 10:41:56 -0300</pubDate><guid>https://rogs.me/2020/05/de-google-my-blog-how-to-blog-in-2020-without-google/</guid><description>&lt;p>Hi everyone!&lt;/p>
&lt;p>Right now I have Google almost completely out of my life, but some of the top commentaries of my
posts in &lt;a href="https://reddit.com/r/degoogle">/r/degoogle&lt;/a> and &lt;a href="https://reddit.com/r/selfhosted">/r/selfhosted&lt;/a> were &amp;ldquo;Your blog still uses google for resources lol&amp;rdquo;,
so I needed to change that.&lt;/p>
&lt;p>&lt;img src="https://rogs.me/2020-05-25-115855.webp" alt="2020-05-25-115855">
&lt;em>Shame.&lt;/em>&lt;/p>
&lt;p>In my old blog, the features that used Google where:&lt;/p>
&lt;ol>
&lt;li>Disquss comments&lt;/li>
&lt;li>The Ghost theme&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Before we begin, I want to let you know that all these fixes have been applied, so you are currently
experiencing the final result.&lt;/strong>&lt;/p>
&lt;h1 id="fixing-comments">Fixing comments&lt;/h1>
&lt;p>I was using disquss to handle comments, since it is what everyone recommends. I checked the network
resources on my site and found something horrible: A bunch of random calls to a bunch of external
addresses, not just Google. Since I care about my reader&amp;rsquo;s privacy, disquss had to go. Someone in
/r/degoogle suggested &amp;ldquo;Commento&amp;rdquo;, and it looked like it was exactly what I needed: A free and opensource,
self-hosted alternative to disquss, and it also had an official docker release!&lt;/p>
&lt;p>According to their &lt;a href="https://docs.commento.io/installation/self-hosting/on-your-server/docker.html">documentation&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;3&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">server&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">registry.gitlab.com/commento/commento&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">5000&lt;/span>:&lt;span style="color:#ae81ff">8080&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">COMMENTO_ORIGIN&lt;/span>: &lt;span style="color:#ae81ff">https://commento.rogs.me&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">COMMENTO_PORT&lt;/span>: &lt;span style="color:#ae81ff">8080&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">COMMENTO_POSTGRES&lt;/span>: &lt;span style="color:#ae81ff">postgres://postgres:mysupersecurepassword@db:5432/commento?sslmode=disable&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">COMMENTO_SMTP_HOST&lt;/span>: &lt;span style="color:#ae81ff">my-mail-host.com&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">COMMENTO_SMTP_PORT&lt;/span>: &lt;span style="color:#ae81ff">587&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">COMMENTO_SMTP_USERNAME&lt;/span>: &lt;span style="color:#ae81ff">mysmtpusername@mail.com&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">COMMENTO_SMTP_PASSWORD&lt;/span>: &lt;span style="color:#ae81ff">mysmtppassword&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">COMMENTO_SMTP_FROM_ADDRESS&lt;/span>: &lt;span style="color:#ae81ff">mysmtpfromaddress@mail.com&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">depends_on&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">db&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">db&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">postgres&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">POSTGRES_DB&lt;/span>: &lt;span style="color:#ae81ff">commento&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">POSTGRES_USER&lt;/span>: &lt;span style="color:#ae81ff">postgres&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">POSTGRES_PASSWORD&lt;/span>: &lt;span style="color:#ae81ff">mysupersecurepassword&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">postgres_data_volume:/var/lib/postgresql/data&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">postgres_data_volume&lt;/span>:
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I created an NGINX reverse proxy for port 5000 and that was it! Commento was up and running without much
issue.&lt;/p>
&lt;p>&lt;img src="https://rogs.me/2020-05-20-105918.webp" alt="2020-05-20-105918">&lt;/p>
&lt;p>Then, I configured my blog and added the simple universal snippet on my blog posts template pages:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#f92672">script&lt;/span> &lt;span style="color:#a6e22e">defer&lt;/span> &lt;span style="color:#a6e22e">src&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://commento.rogs.me/js/commento.js&amp;#34;&lt;/span>&amp;gt;&amp;lt;/&lt;span style="color:#f92672">script&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#f92672">div&lt;/span> &lt;span style="color:#a6e22e">id&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;commento&amp;#34;&lt;/span>&amp;gt;&amp;lt;/&lt;span style="color:#f92672">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="is-that-it">Is that it?&lt;/h1>
&lt;p>Yes, as easy as that. Comments were up and running in less than 20 mins, with no random external resources.
&lt;em>Sweet!&lt;/em>&lt;/p>
&lt;p>&lt;img src="https://rogs.me/2020-05-20-110914.webp" alt="2020-05-20-110914">&lt;/p>
&lt;p>I could even import my old disquss comments in Commento! I was a bit sad to lose them, but they were
just fine!&lt;/p>
&lt;p>&lt;img src="https://rogs.me/2020-05-21-121039.webp" alt="2020-05-21-121039">&lt;/p>
&lt;p>So now that my comments were ready, I could move forward to fixing Ghost&lt;/p>
&lt;h1 id="the-ghost-theme">The Ghost theme&lt;/h1>
&lt;p>This was quite simple. I forked the git repo for my blog theme and began removing everything regarding
Google fonts. You can check the final results here: &lt;a href="https://git.rogs.me/me/blog.rogs.me-ghost-theme">https://git.rogs.me/me/blog.rogs.me-ghost-theme&lt;/a>&lt;/p>
&lt;h2 id="but-thats-not-entirely-it">But that&amp;rsquo;s not entirely it!&lt;/h2>
&lt;p>My ghost blog was big and bloated, and I wasn&amp;rsquo;t really enjoying it anymore, so I wanted to move it to something more
static, fast, and reliable, so I moved everything to Hugo!&lt;/p>
&lt;h2 id="what-is-hugohttpsgohugoio">What is &lt;a href="https://gohugo.io/">Hugo&lt;/a>?&lt;/h2>
&lt;p>According to their website:&lt;/p>
&lt;blockquote>
&lt;p>Hugo is one of the most popular open-source static site generators. With its amazing speed and flexibility, Hugo makes building websites fun again.&lt;/p>
&lt;/blockquote>
&lt;p>So it is a static site generator, that uses .md files for content.&lt;/p>
&lt;h2 id="why-did-i-chose-hugo">Why did I chose Hugo?&lt;/h2>
&lt;ul>
&lt;li>Hugo needs no dependencies. I just had to do &lt;code>sudo pacman -S hugo&lt;/code> and no further dependencies were
needed&lt;/li>
&lt;li>Hugo doesn&amp;rsquo;t need a database since it is &lt;em>static&lt;/em>. That means that my blog could load &lt;em>A LOT&lt;/em> faster
(and it does!)&lt;/li>
&lt;li>Hugo uses a lot less resources than Ghost. For Ghost I needed a docker running with a database, with Hugo
I just serve the files directly with NGINX, just like a regular plain HTML website.&lt;/li>
&lt;/ul>
&lt;h2 id="and-that-is-what-you-are-seeing-right-now">And that is what you are seeing right now!&lt;/h2>
&lt;p>This blog is 100% running with Hugo. Migration was super easy, since Ghost also uses Markdown files. I just needed to match the URLs so old posts wouldn&amp;rsquo;t break and comments worked like they did before. I chose a simple template, migrated, deployed to my server and that was it!&lt;/p>
&lt;p>You can check the code for my blog here: &lt;a href="https://gitlab.com/rogs/rogs.me">https://gitlab.com/rogs/rogs.me&lt;/a>&lt;/p>
&lt;p>My theme: &lt;a href="https://github.com/athul/archie">https://github.com/athul/archie&lt;/a>&lt;/p>
&lt;p>I was pretty satisfied with the migration and how things were coming along.&lt;/p>
&lt;h1 id="one-extra-thing-matomo-analyticshttpsmatomoorg">One extra thing: &lt;a href="https://matomo.org/">Matomo Analytics&lt;/a>&lt;/h1>
&lt;p>For Analytics, of course I wasn&amp;rsquo;t going to use Google Analytics, so Matomo was an easy choice. Here is my
configuration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">app&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">matomo:latest&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">links&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">db&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;./config:/var/www/html/config:rw&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;./logs:/var/www/html/logs&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;9000:80&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">db&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">mariadb:latest&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;./mysql/runtime2:/var/lib/mysql&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;MYSQL_DATABASE=matomo&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;MYSQL_ROOT_PASSWORD=mysupersecurepassword&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;MYSQL_USER=matomo&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;MYSQL_PASSWORD=anothersupersecurepassword&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Again, another reverse proxy for port &lt;code>9000&lt;/code> and Matomo was up and running!&lt;/p>
&lt;p>&lt;img src="https://rogs.me/2020-05-20-114330.webp" alt="2020-05-20-114330">
&lt;img src="https://rogs.me/2020-05-21-121645.webp" alt="2020-05-21-121645">
&lt;em>My blog stats in Matomo&lt;/em>&lt;/p>
&lt;p>Matomo has everything I need, while respecting the users&amp;rsquo; privacy. If you haven&amp;rsquo;t used it before, you should definitely check it out! I have used Google Analytics before, but Matomo seems more powerful to me.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>It isn&amp;rsquo;t easy to run a website outside of Google, but with a little dedication it is possible. With tools
like Matomo and Commento you can easily respect your user&amp;rsquo;s privacy and get away from Google&amp;rsquo;s &amp;ldquo;big
brotherness&amp;rdquo;.&lt;/p>
&lt;p>If you have any further suggestions, I&amp;rsquo;m always looking for more things to self-host and separate
from big corporations.&lt;/p>
&lt;p>Until next time!&lt;/p></description></item><item><title>How I manage multiple development environments in my Django workflow using Docker compose</title><link>https://rogs.me/2020/05/how-i-manage-multiple-development-environments-in-my-django-workflow-using-docker-compose/</link><pubDate>Wed, 13 May 2020 11:36:48 -0300</pubDate><guid>https://rogs.me/2020/05/how-i-manage-multiple-development-environments-in-my-django-workflow-using-docker-compose/</guid><description>&lt;p>Hi everyone!&lt;/p>
&lt;p>Last week I was searching how to manage multiple development environments with the same docker-compose configuration for my Django workflow. I needed to manage a development and a production environment, so this is what I did.&lt;/p>
&lt;p>Some descriptions on my data:&lt;/p>
&lt;ol>
&lt;li>I had around 20 env vars, but some of them where shared among environments.&lt;/li>
&lt;li>I wanted to do it with as little impact as possible.&lt;/li>
&lt;/ol>
&lt;h1 id="first-docker-compose-help-command">First, docker-compose help command&lt;/h1>
&lt;p>The first thing I did was run a simple &lt;code>docker-compose --help&lt;/code>, and it returned this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>Define and run multi-container applications with Docker.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Usage:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> docker-compose &lt;span style="color:#f92672">[&lt;/span>-f &amp;lt;arg&amp;gt;...&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#f92672">[&lt;/span>options&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#f92672">[&lt;/span>COMMAND&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#f92672">[&lt;/span>ARGS...&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> docker-compose -h|--help
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Options:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> -f, --file FILE Specify an alternate compose file
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">(&lt;/span>default: docker-compose.yml&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># more not necessary stuff&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> --env-file PATH Specify an alternate environment file
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I went with the &lt;code>-f&lt;/code> flag, because I also wanted to run some docker images for development. By using the &lt;code>-f&lt;/code> flag I could create a base compose file with the shared env vars (docker-compose.yml) and another one for each of the environments (prod.yml and dev.yml)&lt;/p>
&lt;p>So I went to town. I kept the shared variables inside &lt;code>docker-compose.yml&lt;/code> and added the specific variables and configuration to &lt;code>prod.yml&lt;/code> and &lt;code>dev.yml&lt;/code>&lt;/p>
&lt;p>&lt;code>docker-compose.yml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">app&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">build&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">context&lt;/span>: &lt;span style="color:#ae81ff">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;8000:8000&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">./app:/app&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: &amp;gt;&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> sh -c &amp;#34;python manage.py migrate &amp;amp;&amp;amp; python manage.py runserver 0.0.0.0:8000&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">myvar1=myvar1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">myvar2=myvar2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">myvarx=myvarx&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Since I&amp;rsquo;m going to connect to a remote RDS and a remote Redis, in my &lt;code>prod.yml&lt;/code>, I don&amp;rsquo;t need to define a Postgres or Redis image:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">app&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># DB connections&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">DB_HOST=my-host&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">DB_NAME=db-name&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">DB_USER=db-user&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">DB_PASS=mysupersecurepassword&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For &lt;code>dev.yml&lt;/code> I added the Postgres database image and Redis image:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">app&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">depends_on&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">db&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">redis&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Basics&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">DEBUG=True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># DB connections&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">DB_HOST=db&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">DB_NAME=app&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">DB_USER=postgres&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">DB_PASS=supersecretpassword&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">db&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">postgres:10-alpine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">POSTGRES_DB=app&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">POSRGRES_USER=postgres&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">POSTGRES_PASSWORD=supersecretpassword&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">links&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">redis:redis&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">redis&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">redis:5.0.7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">expose&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;6379&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And to run it between environments, all I have to do is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># For production&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker-compose -f docker-compose.yml -f prod.yml up
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># For development&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker-compose -f docker-compose.yml -f dev.yml up
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it! I have multiple &lt;code>docker-compose&lt;/code> files for all my environments, but I could go even further.&lt;/p>
&lt;h1 id="improving-the-solution">Improving the solution&lt;/h1>
&lt;h2 id="improving-the-base-docker-composeyml-file">Improving the base docker-compose.yml file&lt;/h2>
&lt;p>I liked the way it looked, but I knew I could go deeper. A bunch of vars inside the base &lt;code>docker-compose.yml&lt;/code> looked weird, and made the file a little unreadable. So again, I went to the &lt;code>docker-compose&lt;/code> documentation and found what I needed: &lt;a href="https://docs.docker.com/compose/environment-variables/#the-env-file">env files in docker-compose&lt;/a>.&lt;/p>
&lt;p>So I created a file called &lt;code>globals.env&lt;/code>, and moved all the global env vars to that file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">myvar1=myvar1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">mivar2=myvar2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And on the &lt;code>docker-compose.yml&lt;/code> file I called the &lt;code>globals.env&lt;/code> file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">app&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">env_file&lt;/span>: &lt;span style="color:#ae81ff">globals.env&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is the final result:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">app&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">build&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">context&lt;/span>: &lt;span style="color:#ae81ff">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#e6db74">&amp;#34;8000:8000&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">./app:/app&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: &amp;gt;&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> sh -c &amp;#34;python manage.py migrate &amp;amp;&amp;amp; python manage.py runserver 0.0.0.0:8000&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">env_file&lt;/span>: &lt;span style="color:#ae81ff">globals.env&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="improving-the-running-command">Improving the running command&lt;/h2>
&lt;p>As I mentioned before, I wanted as little impact as possible, and &lt;code>docker-compose -f docker-compose.yml -f envfile.yml up&lt;/code> was a bit long for me. So I created a couple of bash files to ease the ingestion of &lt;code>docker-compose&lt;/code> files:&lt;/p>
&lt;p>&lt;code>prod&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/usr/bin/env bash
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#75715e"># Run django as production&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker-compose -f docker-compose.yml -f prod.yml &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$@&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>dev&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/usr/bin/env bash
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#75715e"># Run django as development&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker-compose -f docker-compose.yml -f dev.yml &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$@&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>&amp;quot;$@&amp;quot;&lt;/code> means &amp;ldquo;Append all extra arguments here&amp;rdquo;, so if I ran the &lt;code>dev&lt;/code> command with the &lt;code>up -d&lt;/code> arguments, the full command would be &lt;code>docker-compose -f docker-compose.yml -f development.yml up -d&lt;/code>, so it is exactly what I wanted&lt;/p>
&lt;p>A quick permissions management:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>chmod +x prod dev
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And now I could run my environments as:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>./dev up
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>./prod up
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="all-good">All good?&lt;/h1>
&lt;p>I was satisfied with this solution. I could run both environments wherever I want with only one command instead of moving env vars all over the place. I could go even further by moving the environment variables of each file to its own &lt;code>.env&lt;/code> file, but I don&amp;rsquo;t think that&amp;rsquo;s needed for the time being. At least I like to know that I can do that down the road if it is necessary&lt;/p></description></item><item><title>Secure your Django API from DDoS attacks with NGINX and fail2ban</title><link>https://rogs.me/2020/04/secure-your-django-api-from-ddos-attacks-with-nginx-and-fail2ban/</link><pubDate>Sun, 26 Apr 2020 11:36:39 -0300</pubDate><guid>https://rogs.me/2020/04/secure-your-django-api-from-ddos-attacks-with-nginx-and-fail2ban/</guid><description>&lt;p>Hello everyone!&lt;/p>
&lt;p>Last week our Django API, hosted on an Amazon EC2 server was attacked by a
botnet farm, which took our services down for almost the entire weekend. I’m not
going to lie, it was a very stressful situation, but at the same time we learned
a lot about how to secure our server from future
&lt;a href="https://en.wikipedia.org/wiki/Denial-of-service_attack">DDoS&lt;/a> attacks.&lt;/p>
&lt;p>For our solution we are using the
&lt;a href="https://www.nginx.com/blog/rate-limiting-nginx/">rate-limiting&lt;/a> functionality from NGINX and
&lt;a href="https://www.fail2ban.org/">fail2ban&lt;/a>, a program that bans external APIs when they break a certain set of
rules. Let’s start!&lt;/p>
&lt;h1 id="nginx-configuration">NGINX configuration&lt;/h1>
&lt;p>In NGINX is simple, we just need to configure the rate-limiting on our website
level.&lt;/p>
&lt;p>We are using Django with the Django REST framework, so we went with a
configuration of 5 requests per second (5r/s) with extra bursts of 5 requests.
This works fine with Django, but you might need to tweak it for your
configuration.&lt;/p>
&lt;p>This allows a total of 10 requests (5 processing and 5 in queue) before our API
returns a 503 server error&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nginx" data-lang="nginx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">limit_req_zone&lt;/span> $binary_remote_addr &lt;span style="color:#e6db74">zone=one:20m&lt;/span> &lt;span style="color:#e6db74">rate=5r/s&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">server&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">limit_req&lt;/span> &lt;span style="color:#e6db74">zone=one&lt;/span> &lt;span style="color:#e6db74">burst=5&lt;/span> &lt;span style="color:#e6db74">nodelay&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Let me explain what is going on here:&lt;/p>
&lt;ol>
&lt;li>First, we are setting up the &lt;code>limit_req_zone&lt;/code>.
&lt;ul>
&lt;li>The &lt;code>$binary_remote_addr&lt;/code> specifies that we are registering the requests by IP&lt;/li>
&lt;li>&lt;code>zone&lt;/code> is the name of the &lt;code>limit_req_zone&lt;/code>, and &lt;code>20m&lt;/code> is its total size&lt;/li>
&lt;li>&lt;code>rate&lt;/code> is the permitted rate per IP address. Here we are allowing &lt;code>5r/s&lt;/code>,
which translates to 1 request every 200ms.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Then, inside the &lt;code>server&lt;/code> directive we use the &lt;code>limit_req_zone&lt;/code> referencing
it by name.
&lt;ul>
&lt;li>&lt;code>zone&lt;/code> specifies the &lt;code>limit_req_zone&lt;/code> to be used, in this case, we named it &lt;code>one&lt;/code>&lt;/li>
&lt;li>&lt;code>burst&lt;/code> is the number of requests that can be queued by the same IP per second,
giving us a grand total of 10 requests per IP (5 in process and 5 in queue)&lt;/li>
&lt;li>We want our queued requests to be processed as soon as possible, by
giving it the &lt;code>nodelay&lt;/code> directive when a slot is freed, an item in the
queue is going to be processed&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;p>If a client goes over the 10 requests limit, NGINX is going to return a 503
(Service Unavailable) error and will record the attempt in the error log. And
here is where it becomes interesting :)&lt;/p>
&lt;p>If you want to read more about NGINX rate limiting, you can check this link &lt;a href="https://www.nginx.com/blog/rate-limiting-nginx/">https://www.nginx.com/blog/rate-limiting-nginx/&lt;/a>&lt;/p>
&lt;h1 id="fail2ban">fail2ban&lt;/h1>
&lt;p>According to their documentation, &lt;a href="https://www.fail2ban.org/">fail2ban&lt;/a> is:&lt;/p>
&lt;blockquote>
&lt;p>Fail2ban scans log files (e.g. /var/log/apache/error_log) and bans IPs that show the malicious signs &amp;ndash; too many password failures, seeking for exploits, etc. Generally Fail2Ban is then used to update firewall rules to reject the IP addresses for a specified amount of time, although any arbitrary other action (e.g. sending an email) could also be configured. Out of the box Fail2Ban comes with filters for various services (apache, courier, ssh, etc).&lt;/p>
&lt;/blockquote>
&lt;p>We are going to use fail2ban to scan our NGINX error logs, and if it finds too
many occurrences of the same IP, it will ban it for an x amount of time.&lt;/p>
&lt;p>First, we need to install fail2ban:&lt;/p>
&lt;p>In Debian based distros:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ sudo apt install fail2ban
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After installing fail2ban, we need to configure our local configuration file. In
fail2ban they are called &amp;ldquo;jails&amp;rdquo;. We can make a local copy with the following
command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Once we have our local configuration file, we can create our own directive. At
the bottom of the &lt;code>/etc/fail2ban/jail.local&lt;/code> file, add this configuration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">[&lt;/span>nginx-req-limit&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>enabled &lt;span style="color:#f92672">=&lt;/span> true
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>filter &lt;span style="color:#f92672">=&lt;/span> nginx-limit-req
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>action &lt;span style="color:#f92672">=&lt;/span> iptables-multiport&lt;span style="color:#f92672">[&lt;/span>name&lt;span style="color:#f92672">=&lt;/span>ReqLimit, port&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;http,https&amp;#34;&lt;/span>, protocol&lt;span style="color:#f92672">=&lt;/span>tcp&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>logpath &lt;span style="color:#f92672">=&lt;/span> /var/log/nginx/error.log
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>findtime &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">5&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>maxretry &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>bantime &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">300&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here is what&amp;rsquo;s happening:&lt;/p>
&lt;ol>
&lt;li>First, we name our filter. In this case, our filter is called
&lt;code>[nginx-req-limit]&lt;/code>&lt;/li>
&lt;li>Then we enable our filter with &lt;code>enabled = true&lt;/code>&lt;/li>
&lt;li>We set our filter to be &lt;code>nginx-limit-req&lt;/code>. This is a default filter from fail2ban&lt;/li>
&lt;li>We define the action that fail2ban is going to do when it finds a suspicious
IP. In this case, it will process it with &lt;code>iptables-multiport&lt;/code> and block the
http and https ports for that IP address&lt;/li>
&lt;li>We tell fail2ban which logfile it is going to scan. In our case, since we are
using NGINX our log file is &lt;code>/var/log/nginx/error.log&lt;/code>&lt;/li>
&lt;li>&lt;code>findtime&lt;/code> is the time in which fail2ban is going to limit the searches, and
maxretry is the max amount of times an IP can appear on the log before it is
banned. In our case, if an IP address appears 2 times in less than 5 seconds on
our error log, fail2ban is going to ban it.&lt;/li>
&lt;li>And finally, we set our &lt;code>bantime&lt;/code> to 300 seconds (5 minutes).&lt;/li>
&lt;/ol>
&lt;p>And that&amp;rsquo;s it! We need to restart fail2ban to see if everything is working
correctly:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ sudo systemctl restart fail2ban.service
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To check if the service is running, you can run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ sudo fail2ban-client status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It should return something like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>Status
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>|- Number of jail: &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">`&lt;/span>- Jail list: nginx-req-limit, sshd
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To know more, you can run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ sudo fail2ban-client status nginx-req-limit
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And it will return something like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>|- Filter
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>| |- Currently failed: &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>| |- Total failed: &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>| &lt;span style="color:#e6db74">`&lt;/span>- File list: /var/log/nginx/error.log
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">`&lt;/span>- Actions
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> |- Currently banned: &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> |- Total banned: &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">`&lt;/span>- Banned IP list: 1.2.3.4
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And that&amp;rsquo;s it! It is fully working, you are now protected from DDoS attacks
dynamically.&lt;/p>
&lt;h1 id="django-configuration">Django configuration&lt;/h1>
&lt;p>For Django things are easy. There is no configuration needed, but if you use the Admin
or REST viewer in any form, you might want to run a separate instance just for that.
In our experience, we got blocked a bunch of times so now we are running the admin and
some cron jobs on a second medium EC2 instance.&lt;/p>
&lt;h1 id="bonus">Bonus&lt;/h1>
&lt;h2 id="what-if-i-want-to-ban-an-ip-address-forever">What if I want to ban an IP address forever?&lt;/h2>
&lt;p>We can create another jail in &lt;code>fail2ban&lt;/code> to achieve this. On our
&lt;code>/etc/fail2ban/jail.local&lt;/code> file, add this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">[&lt;/span>man-ban&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>enabled &lt;span style="color:#f92672">=&lt;/span> true
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>filter &lt;span style="color:#f92672">=&lt;/span> nginx-limit-req
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>action &lt;span style="color:#f92672">=&lt;/span> iptables-multiport&lt;span style="color:#f92672">[&lt;/span>name&lt;span style="color:#f92672">=&lt;/span>ReqLimit, port&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;http,https&amp;#34;&lt;/span>, protocol&lt;span style="color:#f92672">=&lt;/span>tcp&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>logpath &lt;span style="color:#f92672">=&lt;/span> /var/log/nginx/error.log
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>findtime &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>bantime &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">2678400&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>maxretry &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">99999&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This jail wont pick up anything because we are expecting 99999 errors in less
than a second, but it will ban anyone for 1 month. Once we restart fail2ban
again, you can manually ban IP addresses with that jail:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ sudo fail2ban-client set man-ban banip 1.2.3.4
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you check the status, you will see something like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>|- Filter
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>| |- Currently failed: &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>| |- Total failed: &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>| &lt;span style="color:#e6db74">`&lt;/span>- File list: /var/log/nginx/error.log
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">`&lt;/span>- Actions
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> |- Currently banned: &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> |- Total banned: &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">`&lt;/span>- Banned IP list: 1.2.3.4
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The IP &lt;code>1.2.3.4&lt;/code> will be banned for 1 month. You can unban it with&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ sudo fail2ban-client set man-ban unbanip 1.2.3.4
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>In these days, DDoS attacks are very common on the internet, so it is common sense to
be prepared to defend against them.&lt;/p>
&lt;p>In the next delivery we will create a status page for our API, that will let us
know if one of the services is down.&lt;/p>
&lt;p>Stay tuned!&lt;/p></description></item><item><title>My mom was always right | Rant on social media</title><link>https://rogs.me/2020/04/my-mom-was-always-right-rant-on-social-media/</link><pubDate>Sat, 25 Apr 2020 12:35:53 -0300</pubDate><guid>https://rogs.me/2020/04/my-mom-was-always-right-rant-on-social-media/</guid><description>&lt;p>My mom always hated social media. My bother and I always made fun of her because she was always late to all the news. Her main reason against social media was &amp;ldquo;why would I want everyone to know what I&amp;rsquo;m doing? And why should I care what they are doing?&amp;rdquo;. I didn&amp;rsquo;t understand at the time, but now I do.&lt;/p>
&lt;p>I remember when I was 13 years old social media started ramping up. I created a MySpace account to go with the flow. I won&amp;rsquo;t lie, I liked MySpace a lot: creating a website that &amp;ldquo;defined me&amp;rdquo;, sharing my music, posting funny pictures and checking my friends&amp;rsquo; profiles. It all seemed pretty cool and innovative.&lt;/p>
&lt;p>Then it came to Facebook, where you couldn&amp;rsquo;t create your own site or share music players like in MySpace, but you had a wall and your friends could leave messages on your wall! How cool was that? You could share pictures, thoughts, opinions, and your friends didn&amp;rsquo;t have to go to your profile to check it out: Facebook had a feed with all your friends&amp;rsquo; posts, so there was no need to visit their profile just to see what they were doing. It sounded very cool at the moment!&lt;/p>
&lt;p>After that, Twitter. Microblogging, 140 chars max (now its 280 chars, double of what it was before), interactions with people all around the internet, you didn&amp;rsquo;t need be friends with someone to send them a tweet or a private message. Discussions, threads, memes&amp;hellip;&lt;/p>
&lt;p>Instagram. Sharing pictures, stories, following my friends to see their travels, following superstars to see their perfect lifes, ads, paid content.&lt;/p>
&lt;p>Stop.&lt;/p>
&lt;p>Just stop.&lt;/p>
&lt;p>Social media has become too overwhelming in the last couple of years. People are lonelier and more depressed because of social media (&lt;a href="https://guilfordjournals.com/doi/10.1521/jscp.2018.37.10.751">link&lt;/a>). The &lt;a href="https://en.wikipedia.org/wiki/Fear_of_missing_out">FOMO (Fear of missing out)&lt;/a> is at an all-time high.&lt;/p>
&lt;p>Now that I know all of this, &lt;strong>why would I want to be part of something that could make me feel anxiety, depression and have self-esteem issues?&lt;/strong> That was the question I made to myself around 6 months ago. I consider myself an exaggerated person, so I went full in. The goal was to stop using &lt;strong>all&lt;/strong> social media for 1 month and see the results. So here are my thoughts about the experiment&lt;/p>
&lt;h2 id="i-feel-more-relaxed">I feel more relaxed&lt;/h2>
&lt;p>I don&amp;rsquo;t have the need to open my phone when I&amp;rsquo;m at a bus stop or just doing nothing. I don&amp;rsquo;t care what my friends are posting, I don&amp;rsquo;t care if an influencer bought &amp;lsquo;x&amp;rsquo; thing or traveled to &amp;lsquo;y&amp;rsquo; place. Before leaving social media, those things had little impact on me and my daily life, so why should I care?&lt;/p>
&lt;h2 id="i-have-more-free-time-or-time-to-do-more-productive-stuff">I have more free time, or time to do more productive stuff&lt;/h2>
&lt;p>The first week I realized how much time I was wasting using social media, I was wasting between 3 to 4 hours a day in social media, but now I have that time for myself. Now I have built a server for my apartment, I have improved my programming, updated my website, updated and improved my working PC and many other things.&lt;/p>
&lt;h2 id="i-can-appreciate-things-a-lot-more">I can appreciate things a lot more&lt;/h2>
&lt;p>This decision came at the same time as I had to migrate from Venezuela to Uruguay. So, being in a new country I wanted to visit a lot of new places. I went to museums, parks, beaches (in winter, a bit dumb), monuments and many other touristic attractions. It is funny how I was one of the few that enjoyed the moment instead of being neck-deep in my phone. I was free to enjoy the new city I live in.&lt;/p>
&lt;h2 id="my-reach-to-my-pocket-for-my-phone-tick-stopped">My &amp;ldquo;reach to my pocket for my phone&amp;rdquo; tick stopped&lt;/h2>
&lt;p>I wasn&amp;rsquo;t checking my phone as much as before. I could meet and talk to people without my phone being on the table, and I also realized how rude it is to be ignored because everyone at the table is checking Instagram on their phones.&lt;/p>
&lt;h2 id="but-all-of-this-wasnt-always-pretty">But all of this wasn&amp;rsquo;t always pretty&lt;/h2>
&lt;p>I had to make a lot of changes because I depended on social media for many other things:&lt;/p>
&lt;ul>
&lt;li>I installed a Feed aggregator on my PC and added sources for all news I want to watch (and also memes lol). I try to keep it with as few sources as possible, when I see news repeating, I delete the least interesting source. The main difference is that &lt;strong>feed aggregators have endings.&lt;/strong> I check it once a day and never spend more than 10 mins on it.&lt;/li>
&lt;li>My girlfriend now has to find all the &amp;ldquo;Instagram business&amp;rdquo;. Being foreigners on a strange land we sometimes need supplies from home. We have found a few by word of mouth, but we had to resource back to Instagram and check with groups of local Venezuelans.&lt;/li>
&lt;li>I have missed some family pictures, but that is easily fixable. My new cousin was born a month ago, and since I don&amp;rsquo;t use any type of social media, I asked my uncle to send me some pictures and I now have a bunch of pictures of the baby, my other cousins, and more family members. Now they even send me pictures without me asking! The conversations have become more intimate than before, where I would just swipe on a feed and &amp;ldquo;double-tap&amp;rdquo; to like.&lt;/li>
&lt;/ul>
&lt;h2 id="conclusions">Conclusions?&lt;/h2>
&lt;p>My mom was right, as always. The information overload was fun at first, but then it became &lt;em>overwhelming&lt;/em>. Social media has entered our society as a spy and made us dependent on it, and that&amp;rsquo;s bad. We need to get rid of it.&lt;/p>
&lt;p>This experiment started in November 2019 and I can happily say that I left social media for 1 month and never went back. I want to close my Facebook / Instagram accounts (I need to backup my content first) and leave Twitter / LinkedIn because I sometimes use it for work.&lt;/p>
&lt;p>I now recommend everyone I know to shut down everything for a while and see how it feels. They might find out that there is a big world out there if they just move their head up and away from their phones.&lt;/p></description></item><item><title>How to search in a huge table on Django admin</title><link>https://rogs.me/2020/02/17/how-to-search-in-a-huge-table-on-django-admin/</link><pubDate>Mon, 17 Feb 2020 17:08:00 -0400</pubDate><guid>https://rogs.me/2020/02/17/how-to-search-in-a-huge-table-on-django-admin/</guid><description>&lt;div class="kg-card-markdown">
&lt;p>Hello everyone!&lt;/p>
&lt;p>We all know that the Django admin is a super cool tool for Django. You can check your models, and add/edit/delete records from the tables. If you are familiar with Django, I&amp;rsquo;m sure you already know about it.&lt;/p>
&lt;p>I was given a task: Our client wanted to search in a table by one field. It seems easy enough, right? Well, the tricky part is that the table has &lt;strong>523.803.417 records&lt;/strong>.&lt;/p>
&lt;p>Wow. &lt;strong>523.803.417 records&lt;/strong>.&lt;/p>
&lt;p>At least the model was not that complex:&lt;/p>
&lt;p>On &lt;code>models.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">HugeTable&lt;/span>(models&lt;span style="color:#f92672">.&lt;/span>Model):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;Huge table information&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> search_field &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>CharField(max_length&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">10&lt;/span>, db_index&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>, unique&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> is_valid &lt;span style="color:#f92672">=&lt;/span> models&lt;span style="color:#f92672">.&lt;/span>BooleanField(default&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> __str__(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>search_field
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>So for Django admin, it should be a breeze, right? &lt;strong>WRONG.&lt;/strong>&lt;/p>
&lt;h2 id="the-process">The process&lt;/h2>
&lt;p>First, I just added the search field on the admin.py:&lt;/p>
&lt;p>On &lt;code>admin.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">HugeTableAdmin&lt;/span>(admin&lt;span style="color:#f92672">.&lt;/span>ModelAdmin):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> search_fields &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#e6db74">&amp;#39;search_field&amp;#39;&lt;/span>, )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>admin&lt;span style="color:#f92672">.&lt;/span>site&lt;span style="color:#f92672">.&lt;/span>register(HugeTable, HugeTableAdmin)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And it worked! I had a functioning search field on my admin.&lt;br>
&lt;img src="https://rogs.me/2020-02-14-154646.webp" alt="2020-02-14-154646">&lt;/p>
&lt;p>Only one problem: It took &lt;strong>3mins+&lt;/strong> to load the page and &lt;strong>5mins+&lt;/strong> to search. But at least it was working, right?&lt;/p>
&lt;h2 id="wtf">WTF?&lt;/h2>
&lt;p>First, let&amp;rsquo;s split the issues:&lt;/p>
&lt;ol>
&lt;li>Why was it taking +3mins just to load the page?&lt;/li>
&lt;li>Why was it taking +5mins to search if the search field was indexed?&lt;/li>
&lt;/ol>
&lt;p>I started tackling the first one, and found it quite easily: Django was getting only 100 records at a time, but &lt;strong>it had to calculate the length for the paginator and the &amp;ldquo;see more&amp;rdquo; button on the search bar&lt;/strong>&lt;br>
&lt;img src="https://rogs.me/2020-02-14-153605.webp" alt="2020-02-14-153605">&lt;br>
&lt;small>So near, yet so far&lt;/small>&lt;/p>
&lt;h2 id="improving-the-page-load">Improving the page load&lt;/h2>
&lt;p>A quick look at the Django docs told me how to deactivate the &amp;ldquo;see more&amp;rdquo; query:&lt;/p>
&lt;p>&lt;a href="https://docs.djangoproject.com/en/2.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.show_full_result_count">ModelAdmin.show_full_result_count&lt;/a>&lt;/p>
&lt;blockquote>
&lt;p>Set show_full_result_count to control whether the full count of objects should be displayed on a filtered admin page (e.g. 99 results (103 total)). If this option is set to False, a text like 99 results (Show all) is displayed instead.&lt;/p>
&lt;/blockquote>
&lt;p>On &lt;code>admin.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">HugeTableAdmin&lt;/span>(admin&lt;span style="color:#f92672">.&lt;/span>ModelAdmin):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> search_fields &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#e6db74">&amp;#39;search_field&amp;#39;&lt;/span>, )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> show_full_result_count &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>admin&lt;span style="color:#f92672">.&lt;/span>site&lt;span style="color:#f92672">.&lt;/span>register(HugeTable, HugeTableAdmin)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That fixed one problem, but how about the other? It seemed I needed to do my paginator.&lt;/p>
&lt;p>Thankfully, I found an &lt;em>awesome&lt;/em> post by Haki Benita called &lt;a href="https://hakibenita.com/optimizing-the-django-admin-paginator">&amp;ldquo;Optimizing the Django Admin Paginator&amp;rdquo;&lt;/a> that explained exactly that. Since I didn&amp;rsquo;t need to know the records count, I went with the &amp;ldquo;Dumb&amp;rdquo; approach:&lt;/p>
&lt;p>On &lt;code>admin.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.core.paginator &lt;span style="color:#f92672">import&lt;/span> Paginator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> Django.utils.functional &lt;span style="color:#f92672">import&lt;/span> cached_property
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">DumbPaginator&lt;/span>(Paginator):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> Paginator that does not count the rows in the table.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@cached_property&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">count&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">9999999999&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">HugeTableAdmin&lt;/span>(admin&lt;span style="color:#f92672">.&lt;/span>ModelAdmin):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> search_fields &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#e6db74">&amp;#39;search_field&amp;#39;&lt;/span>, )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> show_full_result_count &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> paginator &lt;span style="color:#f92672">=&lt;/span> DumbPaginator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>admin&lt;span style="color:#f92672">.&lt;/span>site&lt;span style="color:#f92672">.&lt;/span>register(HugeTable, HugeTableAdmin)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And it worked! The page was loading blazingly fast :) But the search was still &lt;strong>ultra slow&lt;/strong>. So let&amp;rsquo;s fix that.&lt;br>
&lt;img src="https://rogs.me/2020-02-14-153840.webp" alt="2020-02-14-153840">&lt;/p>
&lt;h2 id="improving-the-search">Improving the search&lt;/h2>
&lt;p>I checked A LOT of options. I almost went with &lt;a href="https://haystacksearch.org/">Haystack&lt;/a>, but it seemed a bit overkill for what I needed. I finally found this super cool tool: &lt;a href="https://github.com/ivelum/djangoql/">djangoql&lt;/a>. It allowed me to search the table by using &lt;em>sql like&lt;/em> operations, so I could search by &lt;code>search_field&lt;/code> and make use of the indexation. So I installed it:&lt;/p>
&lt;p>On &lt;code>settings.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>INSTALLED_APPS &lt;span style="color:#f92672">=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;djangoql&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>On &lt;code>admin.py&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.core.paginator &lt;span style="color:#f92672">import&lt;/span> Paginator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> django.utils.functional &lt;span style="color:#f92672">import&lt;/span> cached_property
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">from&lt;/span> djangoql.admin &lt;span style="color:#f92672">import&lt;/span> DjangoQLSearchMixin
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">DumbPaginator&lt;/span>(Paginator):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> Paginator that does not count the rows in the table.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@cached_property&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">count&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">9999999999&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">HugeTableAdmin&lt;/span>(DjangoQLSearchMixin, admin&lt;span style="color:#f92672">.&lt;/span>ModelAdmin):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> show_full_result_count &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">False&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> paginator &lt;span style="color:#f92672">=&lt;/span> DumbPaginator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>admin&lt;span style="color:#f92672">.&lt;/span>site&lt;span style="color:#f92672">.&lt;/span>register(HugeTable, HugeTableAdmin)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And it worked! By performing the query:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>search_field &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;my search query&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I get my results in around 1 second.&lt;/p>
&lt;p>&lt;img src="https://rogs.me/2020-02-14-154418.webp" alt="2020-02-14-154418">&lt;/p>
&lt;h2 id="is-it-done">Is it done?&lt;/h2>
&lt;p>Yes! Now my client can search by &lt;code>search_field&lt;/code> on a table of 523.803.417 records, very easily and very quickly.&lt;/p>
&lt;p>I&amp;rsquo;m planning to post more Python/Django things I&amp;rsquo;m learning by working with this client, so you might want to stay tuned :)&lt;/p></description></item><item><title>De-Google my life - Part 5 of ¯ (ツ)_/¯: Backups</title><link>https://rogs.me/2019/11/27/de-google-my-life-part-5-of-_-tu-_-backups/</link><pubDate>Wed, 27 Nov 2019 19:30:00 -0400</pubDate><guid>https://rogs.me/2019/11/27/de-google-my-life-part-5-of-_-tu-_-backups/</guid><description>&lt;p>Hello everyone! Welcome to the fifth post of my blog series &amp;ldquo;De-Google my life&amp;rdquo;. If you haven&amp;rsquo;t read the other ones you definitely should! (&lt;a href="https://blog.rogs.me/2019/03/15/de-google-my-life-part-1-of-_-tu-_-why-how/">Part 1&lt;/a>, &lt;a href="https://blog.rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/">Part 2&lt;/a>, &lt;a href="https://blog.rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/">Part 3&lt;/a>, &lt;a href="https://blog.rogs.me/2019/11/20/de-google-my-life-part-4-of-_-tu-_-dokuwiki-ghost/">Part 4&lt;/a>).&lt;/p>
&lt;p>At this point, our server is up and running and everything is working 100% fine, but we can&amp;rsquo;t always trust that. We need a way to securely backup everything in a place where we can restore quickly if needed.&lt;/p>
&lt;h1 id="backup-location">Backup location&lt;/h1>
&lt;p>My backups location was an easy choice. I already had a Wasabi subscription, so why not use it to save my backups as well?&lt;/p>
&lt;p>I created a new bucket on Wasabi, just for my backups and that was it.&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-11-24-18-13-55.webp" alt="Captura-de-pantalla-de-2019-11-24-18-13-55">&lt;br>
&lt;small>There is my bucket, waiting for my &lt;em>sweet sweet&lt;/em> backups&lt;/small>&lt;/p>
&lt;h1 id="security">Security&lt;/h1>
&lt;p>Just uploading everything to Wasabi wasn&amp;rsquo;t secure enough for me, so I&amp;rsquo;m encrypting my tar files with GPG.&lt;/p>
&lt;h2 id="what-is-gpg">What is GPG?&lt;/h2>
&lt;p>From their website:&lt;/p>
&lt;blockquote>
&lt;p>GnuPG (&lt;a href="https://gnupg.org/">GNU Privacy Guard&lt;/a>) is a complete and free implementation of the OpenPGP standard as defined by RFC4880 (also known as PGP). GnuPG allows you to encrypt and sign your data and communications; it features a versatile key management system, along with access modules for all kinds of public key directories. GnuPG, also known as GPG, is a command-line tool with features for easy integration with other applications. A wealth of frontend applications and libraries are available. GnuPG also provides support for S/MIME and Secure Shell (ssh).&lt;/p>
&lt;/blockquote>
&lt;p>So, by using GPG I can encrypt my files before uploading to Wasabi, so if for any reason there is a leak, my files will still be protected by my GPG password.&lt;/p>
&lt;h1 id="script">Script&lt;/h1>
&lt;h2 id="nextcloud">Nextcloud&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Nextcloud&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;======================================&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Backing up Nextcloud&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd /var/lib/docker/volumes/nextcloud_nextcloud/_data/data/roger
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>NEXTCLOUD_FILE_NAME&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>date +&lt;span style="color:#e6db74">&amp;#34;%Y_%m_%d&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>_nextcloud_backup
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo $NEXTCLOUD_FILE_NAME
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Compressing&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>tar czf /root/$NEXTCLOUD_FILE_NAME.tar.gz files/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Encrypting&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>gpg --passphrase-file the/location/of/my/passphrase --batch -c /root/$NEXTCLOUD_FILE_NAME.tar.gz
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Uploading&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>aws s3 cp /root/$NEXTCLOUD_FILE_NAME.tar.gz.gpg s3://backups-cloud/Nextcloud/$NEXTCLOUD_FILE_NAME.tar.gz.gpg --endpoint-url&lt;span style="color:#f92672">=&lt;/span>https://s3.wasabisys.com
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Deleting&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>rm /root/$NEXTCLOUD_FILE_NAME.tar.gz /root/$NEXTCLOUD_FILE_NAME.tar.gz.gpg
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="a-breakdown">A breakdown&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is to specify this is a shell script. The standard for this type of scripts.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Nextcloud&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;======================================&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Backing up Nextcloud&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd /var/lib/docker/volumes/nextcloud_nextcloud/_data/data/roger
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>NEXTCLOUD_FILE_NAME&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>date +&lt;span style="color:#e6db74">&amp;#34;%Y_%m_%d&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>_nextcloud_backup
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo $NEXTCLOUD_FILE_NAME
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here, I &lt;code>cd&lt;/code>ed to where my Nextcloud files are located. On &lt;a href="https://blog.rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/">De-Google my life part 3&lt;/a> I talk about my mistake of not setting my volumes correctly, that&amp;rsquo;s why I have to go to this location. I also create a new filename for my backup file using the current date information.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Compressing&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>tar czf /root/$NEXTCLOUD_FILE_NAME.tar.gz files/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Encrypting&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>gpg --passphrase-file the/location/of/my/passphrase --batch -c /root/$NEXTCLOUD_FILE_NAME.tar.gz
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then, I compress the file into a &lt;code>tar.gz&lt;/code> file. After, it is where the encryption happens. I have a file located somewhere in my server with my GPG password, it is used to encrypt my files using the &lt;code>gpg&lt;/code> command. The command then returns a &amp;ldquo;filename.tar.gz.gpg&amp;rdquo; file, which is then uploaded to Wasabi.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Uploading&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>aws s3 cp /root/$NEXTCLOUD_FILE_NAME.tar.gz.gpg s3://backups-cloud/Nextcloud/$NEXTCLOUD_FILE_NAME.tar.gz.gpg --endpoint-url&lt;span style="color:#f92672">=&lt;/span>https://s3.wasabisys.com
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Deleting&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>rm /root/$NEXTCLOUD_FILE_NAME.tar.gz /root/$NEXTCLOUD_FILE_NAME.tar.gz.gpg
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Finally, I upload everything to Wasabi using &lt;code>awscli&lt;/code> and delete the file, so I keep my filesystem clean.&lt;/p>
&lt;h2 id="is-that-it">Is that it?&lt;/h2>
&lt;p>This is the basic setup for backups, and it is repeated among all my apps, with few variations&lt;/p>
&lt;h2 id="dokuwiki">Dokuwiki&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Dokuwiki&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;======================================&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Backing up Dokuwiki&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd /data/docker
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>DOKUWIKI_FILE_NAME&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>date +&lt;span style="color:#e6db74">&amp;#34;%Y_%m_%d&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>_dokuwiki_backup
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Compressing&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>tar czf /root/$DOKUWIKI_FILE_NAME.tar.gz dokuwiki/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Encrypting&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>gpg --passphrase-file the/location/of/my/passphrase --batch -c /root/$DOKUWIKI_FILE_NAME.tar.gz
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Uploading&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>aws s3 cp /root/$DOKUWIKI_FILE_NAME.tar.gz.gpg s3://backups-cloud/Dokuwiki/$DOKUWIKI_FILE_NAME.tar.gz.gpg --endpoint-url&lt;span style="color:#f92672">=&lt;/span>https://s3.wasabisys.com
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Deleting&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>rm /root/$DOKUWIKI_FILE_NAME.tar.gz /root/$DOKUWIKI_FILE_NAME.tar.gz.gpg
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Pretty much the same as the last one, so here is a quick explanation:&lt;/p>
&lt;ul>
&lt;li>&lt;code>cd&lt;/code> to a folder&lt;/li>
&lt;li>tar it&lt;/li>
&lt;li>encrypt it with gpg&lt;/li>
&lt;li>upload it to a Wasabi bucket&lt;/li>
&lt;li>delete the local files&lt;/li>
&lt;/ul>
&lt;h2 id="ghost">Ghost&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Ghost&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;======================================&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Backing up Ghost&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd /root
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>GHOST_FILE_NAME&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>date +&lt;span style="color:#e6db74">&amp;#34;%Y_%m_%d&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>_ghost_backup
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker container cp ghost_ghost_1:/var/lib/ghost/ $GHOST_FILE_NAME
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker exec ghost_db_1 /usr/bin/mysqldump -u root --password&lt;span style="color:#f92672">=&lt;/span>my-secure-root-password ghost &amp;gt; /root/$GHOST_FILE_NAME/ghost.sql
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Compressing&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>tar czf /root/$GHOST_FILE_NAME.tar.gz $GHOST_FILE_NAME/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Encrypting&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>gpg --passphrase-file the/location/of/my/passphrase --batch -c /root/$GHOST_FILE_NAME.tar.gz
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Uploading&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>aws s3 cp /root/$GHOST_FILE_NAME.tar.gz.gpg s3://backups-cloud/Ghost/$GHOST_FILE_NAME.tar.gz.gpg --endpoint-url&lt;span style="color:#f92672">=&lt;/span>https://s3.wasabisys.com
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Deleting&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>rm -r /root/$GHOST_FILE_NAME.tar.gz $GHOST_FILE_NAME /root/$GHOST_FILE_NAME.tar.gz.gpg
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="a-few-differences">A few differences!&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>docker container cp ghost_ghost_1:/var/lib/ghost/ $GHOST_FILE_NAME
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker exec ghost_db_1 /usr/bin/mysqldump -u root --password&lt;span style="color:#f92672">=&lt;/span>my-secure-root-password ghost &amp;gt; /root/$GHOST_FILE_NAME/ghost.sql
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Something new! Since on Ghost I didn&amp;rsquo;t mount any volumes, I had to get the files directly from the docker container and then get a DB dump for safekeeping. Nothing too groundbreaking, but worth explaining.&lt;/p>
&lt;h1 id="all-done-how-do-i-run-it-automatically">All done! How do I run it automatically?&lt;/h1>
&lt;p>Almost done! I just need to run everything automatically, so I can just set it and forget it. Just like before, whenever I want to run something programatically, I will use a cronjob:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> * * &lt;span style="color:#ae81ff">1&lt;/span> sh /opt/backup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This means:&lt;br>
&lt;em>Please, can you run this script every Monday at 0:00? Thanks, server :&lt;/em>*&lt;/p>
&lt;h1 id="looking-good-does-it-work">Looking good! Does it work?&lt;/h1>
&lt;p>Look for yourself :)&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-11-24-19-26-45.webp" alt="Captura-de-pantalla-de-2019-11-24-19-26-45">&lt;br>
&lt;small>Nextcloud&lt;/small>&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-11-24-19-28-09.webp" alt="Captura-de-pantalla-de-2019-11-24-19-28-09">&lt;br>
&lt;small>Dokuwiki&lt;/small>&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-11-24-19-29-04.webp" alt="Captura-de-pantalla-de-2019-11-24-19-29-04">&lt;br>
&lt;small>Ghost&lt;/small>&lt;/p>
&lt;h1 id="where-do-we-go-from-here">Where do we go from here?&lt;/h1>
&lt;p>I don&amp;rsquo;t know, I only know this project is not over. I have other apps running (Wallabag, Matomo and Commento), but I don&amp;rsquo;t find them as interesting for a new post (of course, if you still want to read about it I will gladly do it).&lt;/p>
&lt;p>I hope you all learned from and enjoyed this experience with me because I sure have! I&amp;rsquo;ve had amazing feedback from the community and that&amp;rsquo;s what always kept this project on my mind.&lt;/p>
&lt;p>A big thank you to &lt;a href="https://reddit.com/r/selfhosted">/r/selfhosted&lt;/a> and more recently &lt;a href="https://www.reddit.com/r/degoogle">/r/degoogle&lt;/a>, I learned A LOT from those communities. If you liked these series, you will definitely like those subreddits.&lt;/p>
&lt;p>I&amp;rsquo;m looking to transform all this knowledge to educational talks soon, so if you are in the Montevideo area, stay tuned for a &lt;em>possible&lt;/em> meetup! (I know this is a longshot in a country of around 4 million people, but worth trying hehe).&lt;/p>
&lt;p>Again, thank you for joining me on this journey and stay tuned! There is more content coming :)&lt;/p></description></item><item><title>De-Google my life - Part 4 of ¯ (ツ)_/¯: Dokuwiki &amp; Ghost</title><link>https://rogs.me/2019/11/20/de-google-my-life-part-4-of-_-tu-_-dokuwiki-ghost/</link><pubDate>Wed, 20 Nov 2019 19:29:00 -0300</pubDate><guid>https://rogs.me/2019/11/20/de-google-my-life-part-4-of-_-tu-_-dokuwiki-ghost/</guid><description>&lt;p>Hello everyone! Welcome to the fourth post of my blogseries &amp;ldquo;De-Google my life&amp;rdquo;. If you haven&amp;rsquo;t read the other ones you definitely should! (&lt;a href="https://blog.rogs.me/2019/03/15/de-google-my-life-part-1-of-_-tu-_-why-how/">Part 1&lt;/a>, &lt;a href="https://blog.rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/">Part 2&lt;/a>, &lt;a href="https://blog.rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/">Part 3&lt;/a>).&lt;/p>
&lt;p>First of all, sorry for the long wait. I had a couple of IRL things to take care of (we will discuss those in further posts, I promise ( ͡° ͜ʖ ͡°)), but now I have plenty of time to work on more blog posts and other projects. Thanks for sticking around, and if you are new, welcome to this journey!&lt;/p>
&lt;p>On this post, we get to the fun part: What am I going to do to improve my online presence? I began with the simplest answer: A blog (this very same blog you are reading right now lol)&lt;/p>
&lt;h1 id="ghost">Ghost&lt;/h1>
&lt;p>&lt;a href="https://ghost.org/">Ghost&lt;/a> is an open source, headless blogging platform made in NodeJS. The community is quite large and most importantly, it fitted all my requirements (Open source and runs in a docker container).&lt;/p>
&lt;p>For the installation, I kept it simple. I went to the &lt;a href="https://hub.docker.com/_/ghost/">DockerHub page for Ghost&lt;/a> and used their base &lt;code>docker-compose&lt;/code> config for myself. This is what I came up with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;3.1&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ghost&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">ghost:1-alpine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">7000&lt;/span>:&lt;span style="color:#ae81ff">2368&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">database__client&lt;/span>: &lt;span style="color:#ae81ff">mysql&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">database__connection__host&lt;/span>: &lt;span style="color:#ae81ff">db&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">database__connection__user&lt;/span>: &lt;span style="color:#ae81ff">root&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">database__connection__password&lt;/span>: &lt;span style="color:#ae81ff">my_super_secure_mysql_password&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">database__connection__database&lt;/span>: &lt;span style="color:#ae81ff">ghost&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">url&lt;/span>: &lt;span style="color:#ae81ff">https://blog.rogs.me&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">db&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">mysql:5.7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">MYSQL_ROOT_PASSWORD&lt;/span>: &lt;span style="color:#ae81ff">my_super_secure_mysql_password&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Simple enough. The base ghost image and a MySQL db image. Simple, readable, functional.&lt;/p>
&lt;p>For the NGINX configuration I used a simple proxy:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nginx" data-lang="nginx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">server&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">listen&lt;/span> &lt;span style="color:#ae81ff">80&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">listen&lt;/span> &lt;span style="color:#e6db74">[::]:80&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">server_name&lt;/span> &lt;span style="color:#e6db74">blog.rogs.me&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">add_header&lt;/span> &lt;span style="color:#e6db74">Strict-Transport-Security&lt;/span> &lt;span style="color:#e6db74">&amp;#34;max-age=15552000&lt;/span>; &lt;span style="color:#f92672">includeSubDomains&amp;#34;&lt;/span> &lt;span style="color:#e6db74">always&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">location&lt;/span> &lt;span style="color:#e6db74">/&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_pass&lt;/span> &lt;span style="color:#e6db74">http://127.0.0.1:7000&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_next_upstream&lt;/span> &lt;span style="color:#e6db74">error&lt;/span> &lt;span style="color:#e6db74">timeout&lt;/span> &lt;span style="color:#e6db74">invalid_header&lt;/span> &lt;span style="color:#e6db74">http_500&lt;/span> &lt;span style="color:#e6db74">http_502&lt;/span> &lt;span style="color:#e6db74">http_503&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Host&lt;/span> $host;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Real-IP&lt;/span> $remote_addr;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Forward-For&lt;/span> $proxy_add_x_forwarded_for;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Forwarded-Proto&lt;/span> &lt;span style="color:#e6db74">https&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_redirect&lt;/span> &lt;span style="color:#66d9ef">off&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_read_timeout&lt;/span> &lt;span style="color:#ae81ff">5m&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">client_max_body_size&lt;/span> &lt;span style="color:#e6db74">10M&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>What does this mean? This config is just &amp;ldquo;Hey NGINX! proxy port 7000 through port 80 please, thanks&amp;rdquo;&lt;/p>
&lt;p>And that was it. So simple, there&amp;rsquo;s nothing much to say. Just like the title of the series,&lt;code>¯\_(ツ)_/¯&lt;/code>&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-11-16-19-52-30.webp" alt="Captura-de-pantalla-de-2019-11-16-19-52-30">&lt;/p>
&lt;p>After that, it was just configuration and setup. I modified &lt;a href="https://github.com/kathyqian/crisp">this theme&lt;/a> to match a little more with my website colors and themes. I think it came out pretty nice :)&lt;/p>
&lt;h1 id="dokuwiki">Dokuwiki&lt;/h1>
&lt;p>I have always admired tech people that have their own wikis. It&amp;rsquo;s like a place where you can find more about them in a fast and easy way: What they use, what their configurations are, tips, cheatsheets, scripts, anything! I don&amp;rsquo;t consider myself someone worthy of a wiki, but I wanted one just for the funsies.&lt;/p>
&lt;p>While doing research, I found &lt;a href="https://www.dokuwiki.org/dokuwiki">Dokuwiki&lt;/a>, which is not only open source, but it uses no database! Everything is kept in files which compose your wiki. P R E T T Y N I C E.&lt;/p>
&lt;p>On this one, DockerHub had no oficial Dokuwiki image, but I used a very good one from the user &lt;a href="https://hub.docker.com/r/mprasil/dokuwiki">mprasil&lt;/a>. I used his recommended configuration (no &lt;code>docker-compose&lt;/code> needed since it was a single docker image):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>docker run -d -p 8000:80 --name my_wiki &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -v /data/docker/dokuwiki/data:/dokuwiki/data &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -v /data/docker/dokuwiki/conf:/dokuwiki/conf &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -v /data/docker/dokuwiki/lib/plugins:/dokuwiki/lib/plugins &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -v /data/docker/dokuwiki/lib/tpl:/dokuwiki/lib/tpl &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -v /data/docker/dokuwiki/logs:/var/log &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> mprasil/dokuwiki
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Some mistakes were made, again&lt;/strong>&lt;br>
I was following instructions blindly, I&amp;rsquo;m dumb. I mounted the Dokuwiki files on the /data/docker directory, which is not what I wanted. In the process of working on this project, I have learned one big thing:&lt;/p>
&lt;p>&lt;em>Always. check. installation. folders and/or mounting points&lt;/em>&lt;/p>
&lt;p>Just like the last one, I didn&amp;rsquo;t want to fix this just for the posts, I&amp;rsquo;m writing about my experience and of course it wasn&amp;rsquo;t perfect.&lt;/p>
&lt;p>Let&amp;rsquo;s continue. Once the docker container was running, I configured NGINX with another simple proxy redirect:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nginx" data-lang="nginx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">server&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">listen&lt;/span> &lt;span style="color:#ae81ff">80&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">listen&lt;/span> &lt;span style="color:#e6db74">[::]:80&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">server_name&lt;/span> &lt;span style="color:#e6db74">wiki.rogs.me&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">add_header&lt;/span> &lt;span style="color:#e6db74">Strict-Transport-Security&lt;/span> &lt;span style="color:#e6db74">&amp;#34;max-age=15552000&lt;/span>; &lt;span style="color:#f92672">includeSubDomains&amp;#34;&lt;/span> &lt;span style="color:#e6db74">always&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">location&lt;/span> &lt;span style="color:#e6db74">/&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_pass&lt;/span> &lt;span style="color:#e6db74">http://127.0.0.1:8000&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_next_upstream&lt;/span> &lt;span style="color:#e6db74">error&lt;/span> &lt;span style="color:#e6db74">timeout&lt;/span> &lt;span style="color:#e6db74">invalid_header&lt;/span> &lt;span style="color:#e6db74">http_500&lt;/span> &lt;span style="color:#e6db74">http_502&lt;/span> &lt;span style="color:#e6db74">http_503&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Host&lt;/span> $host;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Real-IP&lt;/span> $remote_addr;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Forward-For&lt;/span> $proxy_add_x_forwarded_for;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Forwarded-Proto&lt;/span> &lt;span style="color:#e6db74">https&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_redirect&lt;/span> &lt;span style="color:#66d9ef">off&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_read_timeout&lt;/span> &lt;span style="color:#ae81ff">5m&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">client_max_body_size&lt;/span> &lt;span style="color:#e6db74">10M&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Just as the other one: &amp;ldquo;Hey NGINX! Foward port 8000 to port 80 please :) Thanks!&amp;rdquo;&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-11-16-20-15-35.webp" alt="Captura-de-pantalla-de-2019-11-16-20-15-35">&lt;br>
&lt;small>Simple dokuwiki screen, nothing too fancy&lt;/small>&lt;/p>
&lt;p>Again, just like the other one, configuration and setup and voila! Everything was up and running.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>I was getting the hang of this &amp;ldquo;Docker&amp;rdquo; flow. There were mistakes, yes, but nothing too critical that would hurt me in the long run. Everything was running smoothly, and just with a few commands I had everything running and proxied. Just what I wanted.&lt;/p>
&lt;p>Stay tuned for the next delivery, where I&amp;rsquo;m going to talk about GPG encrypted backups to an external Wasabi &amp;ldquo;S3 like&amp;rdquo; bucket. I promise this one won&amp;rsquo;t take 8 months.&lt;/p>
&lt;p>&lt;a href="https://blog.rogs.me/2019/11/27/de-google-my-life-part-5-of-_-tu-_-backups/">Click here for part 5&lt;/a>&lt;/p></description></item><item><title>De-Google my life - Part 3 of ¯ (ツ)_/¯: Nextcloud &amp; Collabora</title><link>https://rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/</link><pubDate>Thu, 28 Mar 2019 19:07:00 -0400</pubDate><guid>https://rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/</guid><description>&lt;div class="kg-card-markdown">
&lt;p>Hello everyone! Welcome to the third post of my blogseries &amp;ldquo;De-Google my life&amp;rdquo;. If you haven&amp;rsquo;t read the other ones you definitely should! (&lt;a href="https://blog.rogs.me/2019/03/15/de-google-my-life-part-1-of-_-tu-_-why-how/">Part 1&lt;/a>, &lt;a href="https://blog.rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/">Part 2&lt;/a>). Today we are moving forward with one of the most important apps I&amp;rsquo;m running on my servers: &lt;a href="https://nextcloud.com/">Nextcloud&lt;/a>. A big part of my Google usage was Google Drive (and all it&amp;rsquo;s derivate apps). With Nextcloud I was looking to replace:&lt;/p>
&lt;ul>
&lt;li>Docs&lt;/li>
&lt;li>Drive&lt;/li>
&lt;li>Photos&lt;/li>
&lt;li>Contacts&lt;/li>
&lt;li>Calendar&lt;/li>
&lt;li>Notes&lt;/li>
&lt;li>Tasks&lt;/li>
&lt;li>More (?)&lt;/li>
&lt;/ul>
&lt;p>I also wanted some new features, like connecting to a S3 bucket directly from my server and have a web interface to interact with it.&lt;/p>
&lt;p>The first step is to set up the server. I&amp;rsquo;m not going to explain that again, but if you want to read more about that, I explain it a bit better on the &lt;a href="https://blog.rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/">second post&lt;/a>&lt;/p>
&lt;h1 id="nextcloud">Nextcloud&lt;/h1>
&lt;h2 id="installation">Installation&lt;/h2>
&lt;p>For my Nextcloud installation I went straight to the &lt;a href="https://github.com/nextcloud/docker">official docker documentation&lt;/a> and extracted this docker compose:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;2&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">nextcloud&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">db&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">db&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">mariadb&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: --&lt;span style="color:#ae81ff">transaction-isolation=READ-COMMITTED --binlog-format=ROW&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">db:/var/lib/mysql&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">MYSQL_ROOT_PASSWORD=my_super_secure_root_password&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">MYSQL_PASSWORD=my_super_secure_password&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">MYSQL_DATABASE=nextcloud&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">MYSQL_USER=nextcloud&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">app&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">image&lt;/span>: &lt;span style="color:#ae81ff">nextcloud&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">8080&lt;/span>:&lt;span style="color:#ae81ff">80&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">links&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">db&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">nextcloud:/var/www/html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">restart&lt;/span>: &lt;span style="color:#ae81ff">always&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Some mistakes were made&lt;/strong>&lt;br>
I forgot to mount the volumes and Docker automatically mounted them in /var/lib/docker/volumes/. This was a small problem I haven&amp;rsquo;t solved yet because it hasn&amp;rsquo;t bringed any serious issues. If someone knows if this is going to be problematic in the long run, please let me know. I didn&amp;rsquo;t wanted to fix this just for the posts, I&amp;rsquo;m writing about my experience and of course it wasn&amp;rsquo;t perfect.&lt;/p>
&lt;p>I created the route &lt;code>/opt/nextcloud&lt;/code> to keep my docker-compose file and finally ran:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>docker-compose pull
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker-compose up -d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It was that simple! The app was running on port 8080! But that is not what I wanted. I wanted it running on port 80 and 443. For that I used a reverse proxy with NGINX and Let&amp;rsquo;s Encrypt&lt;/p>
&lt;h2 id="nginx-configuration">NGINX configuration&lt;/h2>
&lt;p>Configuring NGINX is dead simple. Here is my configuration&lt;/p>
&lt;p>&lt;code>/etc/nginx/sites-available/nextcloud:&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nginx" data-lang="nginx">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">server&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">listen&lt;/span> &lt;span style="color:#ae81ff">80&lt;/span> &lt;span style="color:#e6db74">default_server&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">listen&lt;/span> &lt;span style="color:#e6db74">[::]:80&lt;/span> &lt;span style="color:#e6db74">default_server&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">server_name&lt;/span> &lt;span style="color:#e6db74">myclouddomain.com&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">return&lt;/span> &lt;span style="color:#ae81ff">301&lt;/span> &lt;span style="color:#e6db74">https://&lt;/span>$server_name$request_uri;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">server&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">listen&lt;/span> &lt;span style="color:#ae81ff">443&lt;/span> &lt;span style="color:#e6db74">ssl&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">server_name&lt;/span> &lt;span style="color:#e6db74">myclouddomain.com&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">add_header&lt;/span> &lt;span style="color:#e6db74">Strict-Transport-Security&lt;/span> &lt;span style="color:#e6db74">&amp;#34;max-age=15552000&lt;/span>; &lt;span style="color:#f92672">includeSubDomains&amp;#34;&lt;/span> &lt;span style="color:#e6db74">always&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ssl&lt;/span> &lt;span style="color:#66d9ef">on&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ssl_certificate&lt;/span> &lt;span style="color:#e6db74">/etc/letsencrypt/live/myclouddomain.com/fullchain.pem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ssl_certificate_key&lt;/span> &lt;span style="color:#e6db74">/etc/letsencrypt/live/myclouddomain.com/privkey.pem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">location&lt;/span> &lt;span style="color:#e6db74">/&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_pass&lt;/span> &lt;span style="color:#e6db74">http://127.0.0.1:8080&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_next_upstream&lt;/span> &lt;span style="color:#e6db74">error&lt;/span> &lt;span style="color:#e6db74">timeout&lt;/span> &lt;span style="color:#e6db74">invalid_header&lt;/span> &lt;span style="color:#e6db74">http_500&lt;/span> &lt;span style="color:#e6db74">http_502&lt;/span> &lt;span style="color:#e6db74">http_503&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Host&lt;/span> $host;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Real-IP&lt;/span> $remote_addr;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Forward-For&lt;/span> $proxy_add_x_forwarded_for;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">X-Forwarded-Proto&lt;/span> &lt;span style="color:#e6db74">https&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_redirect&lt;/span> &lt;span style="color:#66d9ef">off&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_read_timeout&lt;/span> &lt;span style="color:#ae81ff">5m&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">location&lt;/span> = &lt;span style="color:#e6db74">/.well-known/carddav&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">return&lt;/span> &lt;span style="color:#ae81ff">301&lt;/span> $scheme://$host/remote.php/dav;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">location&lt;/span> = &lt;span style="color:#e6db74">/.well-known/caldav&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">return&lt;/span> &lt;span style="color:#ae81ff">301&lt;/span> $scheme://$host/remote.php/dav;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Set the client_max_body_size to 1000M so NGINX doesn&amp;#39;t cut uploads
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">client_max_body_size&lt;/span> &lt;span style="color:#e6db74">1000M&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then I created a soft link from the configuration file to the &amp;ldquo;sites enabled&amp;rdquo; folder:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ln -s /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and that was it!&lt;/p>
&lt;p>In this configuration you will see that I&amp;rsquo;m already referencing the SSL certificates even though they don&amp;rsquo;t exist yet. We are going to create them on the next step.&lt;/p>
&lt;h2 id="lets-encrypt-configuration">Let&amp;rsquo;s Encrypt configuration&lt;/h2>
&lt;p>To generate the SSL certificates first you need to point your domain/subdomain to your server. Every DNS manager is different, so you will have to figure that out. The command I will use throught this blog series to create certificates is the following:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo -H certbot certonly --nginx-d mydomain.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The first time you run Let&amp;rsquo;s Encrypt, you have to configure some stuff. They will ask you for your email and some questions. Input that information and finish the process.&lt;/p>
&lt;p>To enable automatic SSL certificates renovation, create a new cron job (&lt;code>crontab -e&lt;/code>) with the following information:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">0&lt;/span> &lt;span style="color:#ae81ff">3&lt;/span> * * * certbot renew -q
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This will run every morning at 3AM and check if any of your domains needs to be renewed. If they do, it will renew it.&lt;/p>
&lt;p>At the end, you should be able to visit &lt;a href="https://myclouddomain.com">https://myclouddomain.com&lt;/a> and be greeted with a nice NextCloud screen:&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-03-28-10-51-04.webp" alt="Captura-de-pantalla-de-2019-03-28-10-51-04">&lt;br>
&lt;small>Beautiful yet frustrating blue screen&lt;/small>&lt;/p>
&lt;h2 id="nextcloud-configuration">Nextcloud configuration&lt;/h2>
&lt;p>In this part I got super stuck. I had everything up and running, but I couldn&amp;rsquo;t get my database to connect. It was SUPER FRUSTRATING. This is why I had failed:&lt;/p>
&lt;p>Since in my docker-compose file I called the MariaDB docker &lt;code>db&lt;/code>, the database host was not &lt;code>localhost&lt;/code> but &lt;code>db&lt;/code>.&lt;/p>
&lt;p>Once that was fixed, Nextcloud was 100% ready to be used!&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-03-28-16-19-13.webp" alt="Captura-de-pantalla-de-2019-03-28-16-19-13">&lt;/p>
&lt;p>After that I went straight to &amp;ldquo;Settings/Basic settings&amp;rdquo; and noticed that my background jobs were set to &amp;ldquo;Ajax&amp;rdquo;. That&amp;rsquo;s not good, because if I don&amp;rsquo;t open the site, the tasks will never run. I changed it to &amp;ldquo;Cron&amp;rdquo; and created a new cron on my server with the following information:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>*/15 * * * * /usr/bin/docker exec --user www-data nextcloud_app_1 php cron.php
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This will run the Nextcloud cronjob in the docker machine every 15 mins.&lt;/p>
&lt;p>Then, in &amp;ldquo;Settings/Overview&amp;rdquo; I noticed a bunch of errors on the &amp;ldquo;Security &amp;amp; setup warnings&amp;rdquo; part. Those were very easy to fix, but since all installations aren&amp;rsquo;t the same I won&amp;rsquo;t go deep into this. &lt;a href="https://duckduckgo.com/">DuckDuckGo&lt;/a> is your friend.&lt;/p>
&lt;h2 id="extra-stuff">Extra stuff&lt;/h2>
&lt;p>The Nextcloud apps store is filled with some interesting applications. The ones I have installed are:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://apps.nextcloud.com/apps/contacts">Contacts&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://apps.nextcloud.com/apps/calendar">Calendar&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://apps.nextcloud.com/apps/notes">Notes&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://apps.nextcloud.com/apps/tasks">Tasks&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://apps.nextcloud.com/apps/files_markdown">Markdown editor&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://apps.nextcloud.com/apps/phonetrack">PhoneTrack&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>But you can add as many as you want! You can check them out &lt;a href="https://apps.nextcloud.com/">here&lt;/a>&lt;/p>
&lt;h1 id="collabora">Collabora&lt;/h1>
&lt;p>Now that NextCloud was up and running, I needed my &amp;ldquo;Google Docs&amp;rdquo; part. Enter Collabora!&lt;/p>
&lt;h2 id="installation-1">Installation&lt;/h2>
&lt;p>If you don&amp;rsquo;t know what it is, Collabora is like Google Docs / Sheets / Slides but free and open source. You can check more about the project &lt;a href="https://nextcloud.com/collaboraonline/">here&lt;/a>&lt;/p>
&lt;p>This was a very easy installation. I ran it directly with docker:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>docker run -t -d -p 127.0.0.1:9980:9980 -e &lt;span style="color:#e6db74">&amp;#39;domain=mynextclouddomain.com&amp;#39;&lt;/span> --restart always --cap-add MKNOD collabora/code
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Created a new NGINX reverse proxy&lt;/p>
&lt;p>&lt;code>/etc/nginx/sites-available/collabora&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-nginx" data-lang="nginx">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Taken from https://icewind.nl/entry/collabora-online/
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">server&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">listen&lt;/span> &lt;span style="color:#ae81ff">443&lt;/span> &lt;span style="color:#e6db74">ssl&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">server_name&lt;/span> &lt;span style="color:#66d9ef">off&lt;/span>&lt;span style="color:#e6db74">ice.mydomain.com&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ssl_certificate&lt;/span> &lt;span style="color:#e6db74">/etc/letsencrypt/live/office.mydomain.com/fullchain.pem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">ssl_certificate_key&lt;/span> &lt;span style="color:#e6db74">/etc/letsencrypt/live/office.mydomain.com/privkey.pem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># static files
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">location&lt;/span> &lt;span style="color:#e6db74">^~&lt;/span> &lt;span style="color:#e6db74">/loleaflet&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_pass&lt;/span> &lt;span style="color:#e6db74">https://localhost:9980&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Host&lt;/span> $http_host;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># WOPI discovery URL
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">location&lt;/span> &lt;span style="color:#e6db74">^~&lt;/span> &lt;span style="color:#e6db74">/hosting/discovery&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_pass&lt;/span> &lt;span style="color:#e6db74">https://localhost:9980&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Host&lt;/span> $http_host;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># main websocket
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">location&lt;/span> ~ &lt;span style="color:#e6db74">^/lool/(.*)/ws$&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_pass&lt;/span> &lt;span style="color:#e6db74">https://localhost:9980&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Upgrade&lt;/span> $http_upgrade;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Connection&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Upgrade&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Host&lt;/span> $http_host;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_read_timeout&lt;/span> &lt;span style="color:#e6db74">36000s&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># download, presentation and image upload
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">location&lt;/span> ~ &lt;span style="color:#e6db74">^/lool&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_pass&lt;/span> &lt;span style="color:#e6db74">https://localhost:9980&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Host&lt;/span> $http_host;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Admin Console websocket
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">location&lt;/span> &lt;span style="color:#e6db74">^~&lt;/span> &lt;span style="color:#e6db74">/lool/adminws&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_pass&lt;/span> &lt;span style="color:#e6db74">https://localhost:9980&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Upgrade&lt;/span> $http_upgrade;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Connection&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Upgrade&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_set_header&lt;/span> &lt;span style="color:#e6db74">Host&lt;/span> $http_host;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">proxy_read_timeout&lt;/span> &lt;span style="color:#e6db74">36000s&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Created the SSL certificate for the collabora installation:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo -H certbot certonly --nginx-d office.mydomain.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And finally I created a soft link from the configuration file to the &amp;ldquo;sites enabled&amp;rdquo; folder:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ln -s /etc/nginx/sites-available/collabora /etc/nginx/sites-enabled
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Pretty easy stuff.&lt;/p>
&lt;h2 id="nextcloud-configuration-1">Nextcloud configuration&lt;/h2>
&lt;p>In Nextcloud I installed &amp;ldquo;Collabora&amp;rdquo; from the &amp;ldquo;Apps&amp;rdquo; menu. On &amp;ldquo;Settings/Collabora Online&amp;rdquo; I added my Collabora URL, applied it and voila!&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-03-28-14-53-08.webp" alt="Captura-de-pantalla-de-2019-03-28-14-53-08">&lt;br>
&lt;small>Sweet Libre Office feel&lt;/small>&lt;/p>
&lt;h1 id="s3-bucket">S3 bucket&lt;/h1>
&lt;p>One of my biggest motivation for this project was a cheap, long term storgage solution for some files I don&amp;rsquo;t interact with every day. I&amp;rsquo;m talking music, movies, videos, ISOS, etc. I used to have a bunch of HDD&amp;rsquo;s but because of all the power outages in Venezuela, almost all my HDDs have died, and new ones are very expensive here, not to say all the issues we have with importing them from the US.&lt;/p>
&lt;p>I wanted to look for something S3 like, but as cheap as possible.&lt;/p>
&lt;p>In my investigations I found &lt;a href="https://wasabi.com/">Wasabi&lt;/a>. Not only it was S3 like, but it was &lt;strong>dirt cheap&lt;/strong>. $6 per month for 1TB of data. 1TB OF DATA FOR $6!! I could not believe it!&lt;/p>
&lt;p>I created an account and installed the &amp;ldquo;external storage support&amp;rdquo; plugin in Nextcloud. After it was installed, I went to &amp;ldquo;Settings/External Storages&amp;rdquo; and filled up the information:&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-03-28-15-32-50.webp" alt="Captura-de-pantalla-de-2019-03-28-15-32-50">&lt;br>
&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-03-28-15-34-18.webp" alt="Captura-de-pantalla-de-2019-03-28-15-34-18">&lt;br>
&lt;small>My bucket name is &amp;ldquo;long-term-storage&amp;rdquo; and my local folder name is &amp;ldquo;Long term storage&amp;rdquo;. You will need to generate API keys for the connection.&lt;/small>&lt;/p>
&lt;p>I applied the changes and that was it! I could not believe how simple it was, so I uploaded a file just to test:&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-03-28-15-38-38.webp" alt="Captura-de-pantalla-de-2019-03-28-15-38-38">&lt;br>
&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-03-28-15-39-12.webp" alt="Captura-de-pantalla-de-2019-03-28-15-39-12">&lt;br>
&lt;small>&lt;a href="https://knowyourmeme.com/memes/noice">Classic &lt;em>noice&lt;/em> meme&lt;/a> uploaded in Nextcloud, ready to download in Wasabi. &lt;em>toungue sound&lt;/em> &lt;strong>Nice&lt;/strong>&lt;/small>&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>The project is looking good! In one sitting I have replaced almost every Google product and even added a humungus amount of storage (virtually infinite!) to the project. For the next delivery I&amp;rsquo;ll add new and fun stuff I always wanted to host myself, like a Wiki, a &lt;a href="https://blog.rogs.me">Blog&lt;/a> (this very same blog!) and many more!&lt;/p>
&lt;p>Stay tuned.&lt;/p>
&lt;p>&lt;a href="https://blog.rogs.me/2019/11/20/de-google-my-life-part-4-of-_-tu-_-dokuwiki-ghost/">Click here for part 4&lt;/a>&lt;br>
&lt;a href="https://blog.rogs.me/2019/11/27/de-google-my-life-part-5-of-_-tu-_-backups/">Click here for part 5&lt;/a>&lt;/p>
&lt;/div></description></item><item><title>De-Google my life - Part 2 of ¯ (ツ)_/¯: Servers and Emails</title><link>https://rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/</link><pubDate>Fri, 22 Mar 2019 21:03:00 -0400</pubDate><guid>https://rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/</guid><description>&lt;p>Hello everyone! Welcome to the second post of this blog series that aims to de-google my life as much as possible. If you haven&amp;rsquo;t read the first one, you should &lt;a href="https://blog.rogs.me/2019/03/15/de-google-my-life-part-1-of-_-tu-_-why-how/">definitely check it out&lt;/a>. On this delivery we&amp;rsquo;ll focus more on code and configurations so I promise you it won&amp;rsquo;t be as boring :)&lt;/p>
&lt;h1 id="servers-configuration">Servers configuration&lt;/h1>
&lt;p>As I mentioned on the previous post, I&amp;rsquo;ll be using two servers that are going to be configured almost the same, so I&amp;rsquo;m going to explain it only one time. In order to host my servers I&amp;rsquo;m using &lt;a href="https://m.do.co/c/cf0ff9cae16a">DigitalOcean&lt;/a> because I&amp;rsquo;m very used to their UI, their prices are excelent and they accept Paypal. If you haven&amp;rsquo;t yet, you should check them out.&lt;/p>
&lt;p>To start, I&amp;rsquo;m using their $5 server which at the time of this writing includes:&lt;/p>
&lt;ul>
&lt;li>Ubuntu 18.04 64 bits&lt;/li>
&lt;li>1GB RAM&lt;/li>
&lt;li>1 CPU&lt;/li>
&lt;li>1000 GB of monthly transfers&lt;/li>
&lt;/ul>
&lt;h2 id="installation">Installation&lt;/h2>
&lt;p>On my first SSH to the server I perform basic tasks such as updating and upgrading the server:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo apt update &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> sudo apt ugrade - y
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then I install some essentials like Ubuntu Common Properties (used to add new repositories using &lt;code>add-apt-repository&lt;/code>) NGINX, HTOP, GIT and Emacs, the best text editor in this planet &lt;small>vim sucks&lt;/small>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo apt install software-properties-common nginx htop git emacs
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For SSL certificates I&amp;rsquo;m going to use Certbot because it is the most simple and usefull tool for it. This one requires some extra steps:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo add-apt-repository ppa:certbot/certbot -y
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo apt update
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo apt install python-certbot-nginx -y
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>By default DigitalOcean servers have no &lt;code>swap&lt;/code>, so I&amp;rsquo;ll add it by pasting some &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-18-04">DigitalOcean boilerplate&lt;/a> on to the terminal:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo fallocate -l 2G /swapfile
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo chmod &lt;span style="color:#ae81ff">600&lt;/span> /swapfile
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo mkswap /swapfile
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo swapon /swapfile
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo cp /etc/fstab /etc/fstab.bak
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#39;/swapfile none swap sw 0 0&amp;#39;&lt;/span> | sudo tee -a /etc/fstab
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo sysctl vm.swappiness&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">10&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo sysctl vm.vfs_cache_pressure&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#ae81ff">50&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo echo &lt;span style="color:#e6db74">&amp;#34;vm.swappiness=10&amp;#34;&lt;/span> &amp;gt;&amp;gt; /etc/sysctl.conf
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo echo &lt;span style="color:#e6db74">&amp;#34;vm.vfs_cache_pressure=50&amp;#34;&lt;/span> &amp;gt;&amp;gt; /etc/sysctl.conf
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This adds 2GB of &lt;code>swap&lt;/code>&lt;/p>
&lt;p>Then I set up my firewall with UFW:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">22&lt;/span> &lt;span style="color:#75715e">#SSH&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">80&lt;/span> &lt;span style="color:#75715e">#HTTP&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">443&lt;/span> &lt;span style="color:#75715e">#HTTPS&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">25&lt;/span> &lt;span style="color:#75715e">#IMAP &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">143&lt;/span> &lt;span style="color:#75715e">#IMAP &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">993&lt;/span> &lt;span style="color:#75715e">#IMAPS&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">110&lt;/span> &lt;span style="color:#75715e">#POP3 &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">995&lt;/span> &lt;span style="color:#75715e">#POP3S&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">587&lt;/span> &lt;span style="color:#75715e">#SMTP&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">465&lt;/span> &lt;span style="color:#75715e">#SMTPS&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw allow &lt;span style="color:#ae81ff">4190&lt;/span> &lt;span style="color:#75715e">#Manage Sieve&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sudo ufw enable
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Finally, I install &lt;code>docker&lt;/code> and &lt;code>docker-compose&lt;/code>, which are going to be the main software running on both servers.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Docker&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl -sSL https://get.docker.com/ | CHANNEL&lt;span style="color:#f92672">=&lt;/span>stable sh
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>systemctl enable docker.service
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>systemctl start docker.service
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Docker compose&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl -L https://github.com/docker/compose/releases/download/&lt;span style="color:#66d9ef">$(&lt;/span>curl -Ls https://www.servercow.de/docker-compose/latest.php&lt;span style="color:#66d9ef">)&lt;/span>/docker-compose-&lt;span style="color:#66d9ef">$(&lt;/span>uname -s&lt;span style="color:#66d9ef">)&lt;/span>-&lt;span style="color:#66d9ef">$(&lt;/span>uname -m&lt;span style="color:#66d9ef">)&lt;/span> &amp;gt; /usr/local/bin/docker-compose
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>chmod +x /usr/local/bin/docker-compose
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now that everything is done, we can continue configuring the first server!&lt;/p>
&lt;h1 id="server-1-mailcow">Server #1: Mailcow&lt;/h1>
&lt;p>For my email I chose Mailcow. Why?&lt;/p>
&lt;ul>
&lt;li>It checks all of my &amp;ldquo;challenges list&amp;rdquo; items from last week&amp;rsquo;s post (&lt;a href="https://github.com/mailcow/mailcow-dockerized">open source and dockerized&lt;/a>).&lt;/li>
&lt;li>The documentation is fantastic, explaining each detail one by one.&lt;/li>
&lt;li>It has a huge community behind it.&lt;/li>
&lt;/ul>
&lt;h2 id="installation--setup">Installation &amp;amp; Setup&lt;/h2>
&lt;p>Installation was simple, first I followed the instructions on their &lt;a href="https://mailcow.github.io/mailcow-dockerized-docs/i_u_m_install/">official documentation&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>cd /opt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git clone https://github.com/mailcow/mailcow-dockerized
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd mailcow-dockerized
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>./generate_config.sh
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># The process will ask you for your FQDN to automatically configure NGINX.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Mine is mail.rogs.me, but yours might be whatever you want&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I pointed my subdomain (an A record in Cloudflare) and I finally opened my browser and visited &lt;a href="https://mail.rogs.me">https://mail.rogs.me&lt;/a> and there it was, beautiful as I was expecting.&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-03-20-17-20-49.webp" alt="Captura-de-pantalla-de-2019-03-20-17-20-49">&lt;br>
&lt;small>What a beautiful cow&lt;/small>&lt;/p>
&lt;p>After that I just followed the documentation to &lt;a href="https://mailcow.github.io/mailcow-dockerized-docs/firststeps-ssl/">configure their Let&amp;rsquo;s Encrypt docker image&lt;/a>, &lt;a href="https://mailcow.github.io/mailcow-dockerized-docs/prerequisite-dns/">added more records on my DNS&lt;/a> and tested a lot with &lt;a href="https://www.mail-tester.com/">https://www.mail-tester.com/&lt;/a> until I got a good score&lt;/p>
&lt;p>&lt;img src="https://rogs.me/Captura-de-pantalla-de-2019-03-20-17-25-14.webp" alt="Captura-de-pantalla-de-2019-03-20-17-25-14">&lt;br>
&lt;small>My actual score. Everything is perfect in self-hosted-mail-land&lt;/small>&lt;/p>
&lt;p>I know that sometimes that score doesn&amp;rsquo;t mean much, but at least is nice to know my email is completely configured.&lt;/p>
&lt;h2 id="backups">Backups&lt;/h2>
&lt;p>Since I keep all my emails local, I didn&amp;rsquo;t want a huge backup solution for this server, so I went with the DigitalOcean backup, which costs $1 per month. Cheap, reliable and it just works.&lt;/p>
&lt;h2 id="edit-nov-23-26-2019">Edit Nov 23-26 2019&lt;/h2>
&lt;p>As of now, I&amp;rsquo;m not using PIA anymore because &lt;a href="https://www.reddit.com/r/homelab/comments/e05ce4/psa_piaprivateinternetaccess_has_been_bought_by/">they where bought by Kape Technologies, which is known for sending malware through their software and for being scummy in general.&lt;/a>. I&amp;rsquo;m now using &lt;a href="https://mullvad.net/">Mullvad&lt;/a>, &lt;a href="https://mullvad.net/es/help/no-logging-data-policy/">which really focuses on security&lt;/a>. If you were using PIA, I really recommend you change providers.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>With all of this my first server was done, but it was also the easiest. This one was a pretty straightforward installation with nothing fancy going on: No backups, no NGINX configuration, nothing much. On the good side, I had my email working really quick and it was a very satisfying and rewarding experience. This is when the &amp;ldquo;selfhost everything&amp;rdquo; bug bit me and this project really started ramp up in speed. On the next post we will talk about the second server, which includes fun stuff as &lt;a href="https://nextcloud.com/">Nextcloud&lt;/a>, &lt;a href="https://www.collaboraoffice.com/">Collabora&lt;/a>, &lt;a href="https://www.dokuwiki.org/dokuwiki">Dokuwiki&lt;/a> and many more.&lt;/p>
&lt;p>Stay tuned!&lt;/p>
&lt;p>&lt;a href="https://blog.rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/">Click here for part 3&lt;/a>&lt;br>
&lt;a href="https://blog.rogs.me/2019/11/20/de-google-my-life-part-4-of-_-tu-_-dokuwiki-ghost/">Click here for part 4&lt;/a>&lt;br>
&lt;a href="https://blog.rogs.me/2019/11/27/de-google-my-life-part-5-of-_-tu-_-backups/">Click here for part 5&lt;/a>&lt;/p></description></item><item><title>De-Google my life - Part 1 of ¯ (ツ)_/¯: Why? How?</title><link>https://rogs.me/2019/03/15/de-google-my-life-part-1-of-_-tu-_-why-how/</link><pubDate>Fri, 15 Mar 2019 15:59:00 -0400</pubDate><guid>https://rogs.me/2019/03/15/de-google-my-life-part-1-of-_-tu-_-why-how/</guid><description>&lt;p>Hi everyone! I&amp;rsquo;m here with my first project of the year. It is almost done, but I think it is time to start documenting everything.&lt;/p>
&lt;p>One day I was hanging out with my girlfriend looking for trips to japan online and found myself bombarded by ads that were disturbingly specific. We realized at the moment that Google knows A LOT of us, and we were not happy about that. With my tech knowledge, I knew that there were a lot of alternatives to Google, but first I needed to answer a bigger question:&lt;/p>
&lt;h1 id="why">Why?&lt;/h1>
&lt;p>I told my techie friends about the craziness I was trying to accomplish and they all answered in unison: Why?&lt;/p>
&lt;p>So I came up with the following list:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Privacy&lt;/strong>. The internet is a scary place if you don&amp;rsquo;t know what you are doing. I don&amp;rsquo;t like big corporations knowing everything about me just to sell ads or use my data for whatever they want. I have learned that if something is free it&amp;rsquo;s because &lt;a href="https://twitter.com/rogergonzalez21/status/1067816233125494784">&lt;strong>you&lt;/strong> are the product&lt;/a> &lt;strong>EXCEPT&lt;/strong> in opensource (thanks to &lt;a href="https://www.reddit.com/user/SnowKissedBerries">/u/SnowKissedBerries&lt;/a> for that clarification.&lt;/li>
&lt;li>&lt;strong>Security&lt;/strong>. I live in a very controlled country (Venezuela). Over here, almost every government agency is looking at you, so using selfhosted alternatives and a VPN is a peace of mind for me and my family.&lt;/li>
&lt;li>&lt;strong>To learn&lt;/strong>. Learning all these skills are going to be good for my career as a Backend / DevOps engineer.&lt;/li>
&lt;li>&lt;strong>Because I can and it is fun&lt;/strong>. Narrowing it all down, I&amp;rsquo;m doing this because I can. It might be overkill, dumb, unreliable &lt;strong>but&lt;/strong> it is really fun. Learning new skills is always a good, fun experience, for me at least.&lt;/li>
&lt;/ul>
&lt;p>Perfect! I have all the &amp;ldquo;Whys&amp;rdquo; detailed, but how am I going to achieve all of this?&lt;/p>
&lt;h1 id="how">How?&lt;/h1>
&lt;p>First of all, I went to the experts (shout out to &lt;a href="https://www.reddit.com/r/selfhosted">/r/selfhosted!&lt;/a>) and read all the interesting topics over there that I could use for my selfhostable endeavours. After 1 week of reading and researching, I came with the following setup:&lt;/p>
&lt;p>2 servers, each one with the following stack:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Server 1: Mail server&lt;/strong>&lt;br>
Mailcow for my SMTP / IMAP email server&lt;/li>
&lt;li>&lt;strong>Server 2: Everything-else server&lt;/strong>&lt;br>
Nextcloud for my files, calendar, tasks and contacts&lt;br>
Some other apps (?) (More on that for the following posts)&lt;/li>
&lt;/ul>
&lt;p>I chose DigitalOcean for the hosting because it is cheap and I have a ton of experience with those servers (I have setup more than 100 servers on their platform).&lt;/p>
&lt;p>For VPN I chose &lt;a href="https://www.privateinternetaccess.com/pages/buy-a-vpn/1218buyavpn?invite=U2FsdGVkX1_cGyzYzdmeUMjhrUAwTzDBCMY-PsW-pXA%2CSawh3XnBRwlSt_9084reCHGX1Kk">PIA&lt;/a>. The criteria for this decision was that one of my friends borrowed me his account for ~2 weeks and it worked super quick. Sometimes I didn&amp;rsquo;t realize I was connected to the VPN on because the internet was super fast.&lt;/p>
&lt;h1 id="some-self-imposed-challenges">Some self-imposed challenges&lt;/h1>
&lt;p>I knew this wasn&amp;rsquo;t going to be easy, so of course I added more challenges just because &lt;s>I&amp;rsquo;m dumb&lt;/s>.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Only use open source software&lt;/strong>&lt;br>
I wasn&amp;rsquo;t going to install more proprietary software on my servers. I wanted free and open source alternatives for my setup.&lt;/li>
&lt;li>&lt;strong>Only use Docker&lt;/strong>&lt;br>
I had &amp;ldquo;Learn docker&amp;rdquo; in my backlog for too long, so I used this opportunity to learn it the hard way.&lt;/li>
&lt;li>&lt;strong>Use a cheap but reliable backup solution&lt;/strong>&lt;br>
One of the parts that scared me about having my own servers was the backups. If one of the servers goes down, almost all of my work goes with it, so I needed to have a reliable but cheap backup solution.&lt;/li>
&lt;/ul>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>This is only the first part, but I&amp;rsquo;m planning on this being a long and very cool project. I hope I didn&amp;rsquo;t bore you to death with all my yapping, I promise my next post will be more entertaining with code, server configurations, and all of that good stuff.&lt;/p>
&lt;p>&lt;a href="https://blog.rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/">Click here for part 2&lt;/a>&lt;br>
&lt;a href="https://blog.rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/">Click here for part 3&lt;/a>&lt;br>
&lt;a href="https://blog.rogs.me/2019/11/20/de-google-my-life-part-4-of-_-tu-_-dokuwiki-ghost/">Click here for part 4&lt;/a>&lt;br>
&lt;a href="https://blog.rogs.me/2019/11/27/de-google-my-life-part-5-of-_-tu-_-backups/">Click here for part 5&lt;/a>&lt;/p></description></item></channel></rss>