ATProto Browser

ATProto Browser

Experimental browser for the Atmosphere

Record data

{
  "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&amp;family=Atkinson+Hyperlegible+Next:ital,wght@0,200..800;1,200..800&amp;family=Bungee+Shade&amp;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>: &gt;-<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>: &gt;-<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>: &gt;-<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>: &gt;-<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>"
  }
}