Experimental browser for the Atmosphere
{ "uri": "at://did:plc:6ll5xi67lyuyovt6fiv4fnjo/industries.geesawra.website/0f914d3f08d9d19b504b6d1b4affa9ca43fabf1414a49a8bc02cd9dfaef414db", "cid": "bafyreibias2zac7b5n3t5vqbqqrsyzorykxopz2ankfnmfts5wtevqfij4", "value": { "$type": "industries.geesawra.website", "title": "How I run my ATProto PDS | geesawra.industries", "embeds": [ { "$type": "blob", "ref": { "$link": "bafkreigenauclkihgxjiqkciyum46maffnsdqxmhidr223jnzq67ae2jfu" }, "mimeType": "*/*", "size": 1017 }, { "$type": "blob", "ref": { "$link": "bafkreidamltn7c4xcbt24vpgkgbxkmjcneldzazvmlmpva3cps7zvylini" }, "mimeType": "*/*", "size": 148 }, { "$type": "blob", "ref": { "$link": "bafkreiao2jv7wrvyrwicczk3cecvoccs7r3qrt2c4ci2lvx2bs6on6xv7u" }, "mimeType": "*/*", "size": 1988 } ], "content": "<!DOCTYPE html><html lang=\"en-us\"><head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>How I run my ATProto PDS | geesawra.industries</title>\n <link rel=\"stylesheet\" href=\"/at/geesawra.industries/blobs/bafkreigenauclkihgxjiqkciyum46maffnsdqxmhidr223jnzq67ae2jfu\">\n <link rel=\"stylesheet\" href=\"/at/geesawra.industries/blobs/bafkreidamltn7c4xcbt24vpgkgbxkmjcneldzazvmlmpva3cps7zvylini\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin=\"\">\n<link href=\"https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:ital,wght@0,200..800;1,200..800&family=Atkinson+Hyperlegible+Next:ital,wght@0,200..800;1,200..800&family=Bungee+Shade&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/at/geesawra.industries/blobs/bafkreiao2jv7wrvyrwicczk3cecvoccs7r3qrt2c4ci2lvx2bs6on6xv7u\">\n<script type=\"module\" src=\"https://cdn.jsdelivr.net/npm/@daviddarnes/bluesky-post\"></script>\n\n </head>\n\n <body>\n <nav>\n <ul class=\"menu\">\n \n <li><a href=\"/\">Home</a></li>\n \n <li><a href=\"/at/geesawra.industries/industries.geesawra.website/df50555e2c1437af886a0c0e56855ac376a853f0b7e99f89b0fc4dc98b2b6e29\">Posts</a></li>\n \n </ul>\n <hr>\n </nav>\n\n<div class=\"article-meta\">\n<h1><span class=\"title\">How I run my ATProto PDS</span></h1>\n<h3 class=\"author\">Authored by geesawra</h3>\n<h3 class=\"date\">2025/04/29</h3>\n</div>\n\n<main>\n<p>Hi!</p>\n<p>I run my own <a href=\"https://atproto.com/guides/overview\">ATProto</a> <a href=\"https://atproto.com/guides/glossary#pds-personal-data-server\">PDS</a>, for various reasons:</p>\n<ul>\n<li>I want to own my data.</li>\n<li>It’s a good way to make the ATProto network resilient against centralized infrastructure.</li>\n<li>I’m a nerd.</li>\n</ul>\n<p>Last year I set myself a challenge: buy the cheapest VPS during the Black Friday deals and host my PDS on it.</p>\n<template id=\"bluesky-post-template\">\n<div class=\"bskypost\">\n <blockquote>\n <div id=\"post-content\">\n <p>\n </p><div id=\"author\">\n <img data-key=\"author.avatar\" class=\"post-image\">\n <div id=\"author-text\">\n <span data-key=\"author.displayName\">Slow connection? No problem, <a href=\"https://bsky.app/profile/geesawra.industries/post/3lc7bzgmf2s25\">link here</a>!</span> • <a data-key=\"url\">@<span data-key=\"username\"></span></a>\n </div>\n <p></p>\n </div>\n <p data-key=\"record.text\" class=\"post-text\"></p>\n <p style=\"text-align: right\"><a href=\"https://bsky.app/profile/geesawra.industries/post/3lc7bzgmf2s25\">Original skeet</a></p>\n </div>\n </blockquote>\n <p></p>\n</div>\n</template>\n\n<bluesky-post>\n <a href=\"https://bsky.app/profile/geesawra.industries/post/3lc7bzgmf2s25\"></a>\n</bluesky-post>\n\n<p>It hasn’t been a flawless experience — mostly due to deliberately choosing cheap hosting — but the process gave me the knowledge and confidence to host my main ATProto account on it: this post explains how I did it.</p>\n<p>PDS hosting doesn’t require many resources: posts are lightweight, the only media you’re storing is your own, and the nature of the relay architecture means you have limited traffic aimed directly at your server — except if you’re <a href=\"/at/geesawra.industries/industries.geesawra.website/7f2633d8d2e059e30a789516935b6d4dcc3cd2ecabc6dec32d7f94a4ff0260f6\">hosting your own website</a> on it!</p>\n<p>As a huge <a href=\"https://alpinelinux.org/\">Alpine Linux</a> fan, I installed it through the VPS administration panel, and as expected, it has performed flawlessly.</p>\n<p>The entire stack is executed on rootless <a href=\"https://podman.io/\">Podman</a>, managed remotely through <a href=\"https://www.portainer.io/\">Portainer</a>: I use a variation of the <a href=\"https://github.com/bluesky-social/pds\">official</a> Bluesky <a href=\"https://github.com/bluesky-social/pds/blob/main/compose.yaml\"><code>compose.yml</code></a> file that I extracted their repository.</p>\n<p>I don’t like the <code>install.sh</code> approach as it feels brittle and hard to maintain long-term: I run many services on my machines, all of them containerized, to avoid messing with the host configuration files and runtime.</p>\n<p>I kept Caddy as my reverse proxy of choice because it’s set-and-forget once configured properly, and the setup Bluesky provides just works.</p>\n<p>Bluesky suggests the use of <code>containrrr/watchtower</code>, but I’d advise against it: I’ve been bitten in the past by indiscriminate upgrades, mostly due to pulling Docker images with the <code>latest</code> tag, and I’d like not to repeat that experience anymore.</p>\n<p>Instead of automated updates, I subscribed to Bluesky’s Github notifications to keep myself up to date with new releases.</p>\n<blockquote>\n<p>My philosophy for this deployment is to do the best you can: posting on Bluesky is <em>not a vital service</em> for me, I’m okay with a few hours of downtime.</p></blockquote>\n<p>So far the PDS software seems to be managed in a sane way, following <a href=\"https://semver.org\">semver</a> best practices.</p>\n<p>I never had issues stemming from <code>docker pull</code>ing every once in a while.</p>\n<p>The only problem I encountered was due to the host system clock losing track of time, which I promptly solved by enabling an NTP daemon.</p>\n<p>To mitigate the risk of data loss, I backup the data directory on <a href=\"https://borgbase.com\">Borgbase</a> with <a href=\"https://restic.net/\">Restic</a> every few hours — I also schedule regular scrubs and checks with the same Docker image.</p>\n<p>Without further ado, here’s the <code>compose.yml</code> file:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"><code class=\"language-yml\" data-lang=\"yml\"><span style=\"display:flex;\"><span><span style=\"color:#f92672\">version</span>: <span style=\"color:#e6db74\">\"3.9\"</span>\n</span></span><span style=\"display:flex;\"><span><span style=\"color:#f92672\">services</span>:\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">pds</span>:\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">container_name</span>: <span style=\"color:#ae81ff\">pds</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">image</span>: <span style=\"color:#ae81ff\">ghcr.io/bluesky-social/pds:0.4</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">restart</span>: <span style=\"color:#ae81ff\">unless-stopped</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">env_file</span>: <span style=\"color:#e6db74\">\"./stack.env\"</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">volumes</span>:\n</span></span><span style=\"display:flex;\"><span> - <span style=\"color:#f92672\">type</span>: <span style=\"color:#ae81ff\">bind</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">source</span>: <span style=\"color:#ae81ff\">${DATADIR}</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">target</span>: <span style=\"color:#ae81ff\">/pds</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">caddy</span>:\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">image</span>: <span style=\"color:#ae81ff\">caddy:latest</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">restart</span>: <span style=\"color:#ae81ff\">unless-stopped</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">command</span>: <span style=\"color:#ae81ff\">caddy run --config /etc/caddy/Caddyfile</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">ports</span>:\n</span></span><span style=\"display:flex;\"><span> - <span style=\"color:#e6db74\">\"80:80\"</span>\n</span></span><span style=\"display:flex;\"><span> - <span style=\"color:#e6db74\">\"443:443\"</span>\n</span></span><span style=\"display:flex;\"><span> - <span style=\"color:#e6db74\">\"443:443/udp\"</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">volumes</span>:\n</span></span><span style=\"display:flex;\"><span> - <span style=\"color:#ae81ff\">${DATADIR}/geesawra.industries:/geesawra.industries</span>\n</span></span><span style=\"display:flex;\"><span> - <span style=\"color:#ae81ff\">${DATADIR}/caddy/Caddyfile:/etc/caddy/Caddyfile</span>\n</span></span><span style=\"display:flex;\"><span> - <span style=\"color:#ae81ff\">${DATADIR}/caddy/data:/data</span>\n</span></span><span style=\"display:flex;\"><span> - <span style=\"color:#ae81ff\">${DATADIR}/caddy/config:/config</span>\n</span></span><span style=\"display:flex;\"><span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">backup</span>:\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">image</span>: <span style=\"color:#ae81ff\">mazzolino/restic</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">hostname</span>: <span style=\"color:#ae81ff\">docker</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">restart</span>: <span style=\"color:#ae81ff\">unless-stopped</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">environment</span>:\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RUN_ON_STARTUP</span>: <span style=\"color:#e6db74\">\"true\"</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">BACKUP_CRON</span>: <span style=\"color:#e6db74\">\"*/45 * * * *\"</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RESTIC_CHECK_ARGS</span>: >-<span style=\"color:#e6db74\">\n</span></span></span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\"> --read-data-subset=10%</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RESTIC_REPOSITORY</span>: <span style=\"color:#ae81ff\">rest:[REDACTED]</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RESTIC_PASSWORD</span>: [<span style=\"color:#ae81ff\">REDACTED]</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RESTIC_BACKUP_SOURCES</span>: <span style=\"color:#ae81ff\">/mnt/pds</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RESTIC_BACKUP_ARGS</span>: >-<span style=\"color:#e6db74\">\n</span></span></span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\"> --tag pds</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RESTIC_FORGET_ARGS</span>: >-<span style=\"color:#e6db74\">\n</span></span></span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\"> --keep-last 10\n</span></span></span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\"> --keep-daily 7\n</span></span></span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\"> --keep-weekly 5\n</span></span></span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\"> --keep-monthly 12</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">TZ</span>: <span style=\"color:#ae81ff\">Europe/Berlin</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">volumes</span>:\n</span></span><span style=\"display:flex;\"><span> - <span style=\"color:#ae81ff\">${DATADIR}:/mnt/pds:ro</span>\n</span></span><span style=\"display:flex;\"><span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">prune</span>:\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">image</span>: <span style=\"color:#ae81ff\">mazzolino/restic</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">hostname</span>: <span style=\"color:#ae81ff\">docker</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">restart</span>: <span style=\"color:#ae81ff\">unless-stopped</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">environment</span>:\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">SKIP_INIT</span>: <span style=\"color:#e6db74\">\"true\"</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RUN_ON_STARTUP</span>: <span style=\"color:#e6db74\">\"false\"</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">PRUNE_CRON</span>: <span style=\"color:#e6db74\">\"0 0 4 * * *\"</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RESTIC_REPOSITORY</span>: <span style=\"color:#ae81ff\">rest:[REDACTED]</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RESTIC_PASSWORD</span>: [<span style=\"color:#ae81ff\">REDACTED]</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">TZ</span>: <span style=\"color:#ae81ff\">Europe/Berlin</span>\n</span></span><span style=\"display:flex;\"><span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">check</span>:\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">image</span>: <span style=\"color:#ae81ff\">mazzolino/restic</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">hostname</span>: <span style=\"color:#ae81ff\">docker</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">restart</span>: <span style=\"color:#ae81ff\">unless-stopped</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">environment</span>:\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">SKIP_INIT</span>: <span style=\"color:#e6db74\">\"true\"</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RUN_ON_STARTUP</span>: <span style=\"color:#e6db74\">\"false\"</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">CHECK_CRON</span>: <span style=\"color:#e6db74\">\"0 15 5 * * *\"</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RESTIC_CHECK_ARGS</span>: >-<span style=\"color:#e6db74\">\n</span></span></span><span style=\"display:flex;\"><span><span style=\"color:#e6db74\"> --read-data-subset=10%</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RESTIC_REPOSITORY</span>: <span style=\"color:#ae81ff\">rest:[REDACTED]</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">RESTIC_PASSWORD</span>: [<span style=\"color:#ae81ff\">REDACTED]</span>\n</span></span><span style=\"display:flex;\"><span> <span style=\"color:#f92672\">TZ</span>: <span style=\"color:#ae81ff\">Europe/Berlin</span>\n</span></span></code></pre></div><p>There are two notable features in this <code>compose.yml</code>:</p>\n<ul>\n<li>As <code>pds</code> is configured through environment variables, I’m storing them in a <code>stack.env</code> — this file is auto-generated by Portainer, managed through its web UI.</li>\n<li>I’m binding the host volume path to <code>${DATADIR}</code>, which is defined in <code>stack.env</code>.</li>\n</ul>\n<p>Running a PDS isn’t exactly a walk in the park, but if you’ve played around with containers before and feel at home in the Linux shell, you should be good to go.</p>\n<p>On a positive note, migrating my account from <code>bsky.social</code> to my own PDS has been a great experience, and it’s certainly something the ActivityPub folks should look into for their next protocol iteration.</p>\n<template id=\"bluesky-post-template\">\n<div class=\"bskypost\">\n <blockquote>\n <div id=\"post-content\">\n <p>\n </p><div id=\"author\">\n <img data-key=\"author.avatar\" class=\"post-image\">\n <div id=\"author-text\">\n <span data-key=\"author.displayName\">Slow connection? No problem, <a href=\"https://bsky.app/profile/geesawra.industries/post/3l772nfieyk2t\">link here</a>!</span> • <a data-key=\"url\">@<span data-key=\"username\"></span></a>\n </div>\n <p></p>\n </div>\n <p data-key=\"record.text\" class=\"post-text\"></p>\n <p style=\"text-align: right\"><a href=\"https://bsky.app/profile/geesawra.industries/post/3l772nfieyk2t\">Original skeet</a></p>\n </div>\n </blockquote>\n <p></p>\n</div>\n</template>\n\n<bluesky-post>\n <a href=\"https://bsky.app/profile/geesawra.industries/post/3l772nfieyk2t\"></a>\n</bluesky-post>\n\n<template id=\"bluesky-post-template\">\n<div class=\"bskypost\">\n <blockquote>\n <div id=\"post-content\">\n <p>\n </p><div id=\"author\">\n <img data-key=\"author.avatar\" class=\"post-image\">\n <div id=\"author-text\">\n <span data-key=\"author.displayName\">Slow connection? No problem, <a href=\"https://bsky.app/profile/geesawra.industries/post/3l772vddndc2t\">link here</a>!</span> • <a data-key=\"url\">@<span data-key=\"username\"></span></a>\n </div>\n <p></p>\n </div>\n <p data-key=\"record.text\" class=\"post-text\"></p>\n <p style=\"text-align: right\"><a href=\"https://bsky.app/profile/geesawra.industries/post/3l772vddndc2t\">Original skeet</a></p>\n </div>\n </blockquote>\n <p></p>\n</div>\n</template>\n\n<bluesky-post>\n <a href=\"https://bsky.app/profile/geesawra.industries/post/3l772vddndc2t\"></a>\n</bluesky-post>\n\n<p>Until next time!</p>\n\n</main>\n\n <footer>\n \n \n <hr>\n © <a href=\"https://geesawra.industries\">geesawra</a> 2024 – 2025 | <a href=\"https://github.com/geesawra\">Github</a> | <a href=\"https://bsky.app/profile/geesawra.industries\">Bluesky</a>\n \n </footer>\n \n\n\n</body></html>" } }