惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
罗磊的独立博客
宝玉的分享
宝玉的分享
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
V2EX
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tailwind CSS Blog
博客园_首页
量子位
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
人人都是产品经理
人人都是产品经理
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
S
SegmentFault 最新的问题
雷峰网
雷峰网
小众软件
小众软件
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
Jina AI
Jina AI
Hugging Face - Blog
Hugging Face - Blog

jola.dev

Migrating your Bluesky account the hard way | jola.dev Cluster singleton pattern | jola.dev Speeding up a Phoenix LiveView web app with a CDN | jola.dev Self-hosting an atproto container registry | jola.dev Migrating to the new Tangled knot2 | jola.dev Self-hosting and Tangled | jola.dev Self-hosting your PDS | jola.dev Taking control of your atproto account | jola.dev No cost, no value | jola.dev Latch - an Elixir atproto OAuth library | jola.dev Limited output is a feature | jola.dev Distributed rate limiter with HRW in Elixir | jola.dev A computer can never be held accountable | jola.dev Elixir Cluster 101 | jola.dev How to stop Claude from saying load-bearing | jola.dev Let libraries be libraries | jola.dev CI workflows on Tangled for Elixir | jola.dev Automatically syncing your blog to atproto and standard.site | jola.dev Appreciation for the small web | jola.dev Treating LLMs as programming books Publishing your blog to standard.site in Elixir Generating OG images in Elixir The social contract of writing Highest Random Weight in Elixir bunnyx: a bunny.net Elixir client library Building for the joy of building Running local models on an M4 with 24GB memory How to hit your Claude weekly limit so you can go outside and touch grass Dropping Cloudflare for bunny.net Building a blog with Elixir and Phoenix
cove.town, atproto self-hosted self-hosting | jola.dev
https://jola.dev/about · 2026-08-28 · via jola.dev

A while ago I shared cove.town as an experiment with atproto self-hosting. It’s driven by a bunch of little docker composes and contains a PDS, a knot, a spindle and a hold. The PDS owns the accounts, the knot owns the repo itself, the spindle builds the images it runs on, and the hold is the docker registry the composes point at. It’s a tiny little almost self-sufficient atproto ecosystem.

I’ve now shared write ups for how to get all these components set up, step by step, so I thought I’d do one final post tying it all together.

Building the images

The composes run on my Dokploy server, and each one points at a Docker image built on the spindle. I would love to use the newer microvm engine, but like most hosting providers, Hetzner (referral link for €20 off) does not provide access to /dev/kvm, which seems like a precondition for using microvm if you don’t want your workflows to take forever using qemu emulation. Instead I’m using the original nixery engine and I’m cheekily giving the workflow access to the docker socket I'm using kaniko to build the images without a docker socket! Anyway, we also have access controls for the spindle and the fact that non-members creating a PR does not run any workflows.

So let’s take a look at one. They’re all very similar so it doesn’t matter much which one. Here’s the PDS:

when:

- event: ["push"]

branch: ["main"]

paths:

- ".tangled/workflows/pds.yml"

engine: nixery

environment:

VERSION: bc751b0ee2fef8dfebb5c36775b5aa672e5d086d

KANIKO_IMAGE: ghcr.io/osscontainertools/kaniko:v1.28.3@sha256:779f463aaa3219151ceff518249aa043a60bcb09a17fab58b915f63c678ecde6

dependencies:

nixpkgs:

- go-containerregistry

- gnutar

github:nixos/nixpkgs:

- bash

- curl

- cacert

steps:

- name: build and push

command: |

set -euo pipefail

crane export --platform linux/arm64 "$KANIKO_IMAGE" - | tar -xf - -C /tmp kaniko/executor

export DOCKER_CONFIG=/tmp/docker-config

mkdir -p "$DOCKER_CONFIG"

echo "$CI_BOT_PASSWORD" | /tmp/kaniko/executor login atcr.io -u cove.town --password-stdin

git clone https://tangled.org/tranquil.farm/tranquil-pds.git /tmp/src

git -C /tmp/src checkout "$VERSION"

/tmp/kaniko/executor \

--context /tmp/src \

--ignore-path /tmp/src \

--ignore-path /tmp/docker-config \

--ignore-path /tangled \

--ignore-path /nix \

--build-arg "DISTROLESS_IMAGE=gcr.io/distroless/cc-debian13:latest@sha256:a017e74bd2a12d98342dbecd33d121d2b160415ed777573dc1808969e989d94d" \

--destination "atcr.io/cove.town/tranquil-pds:$VERSION" \

--destination "atcr.io/cove.town/tranquil-pds:latest"

- name: trigger redeploy

environment:

SSL_CERT_FILE: /tangled/home/.nix-profile/etc/ssl/certs/ca-bundle.crt

command: |

curl -fsS -X POST "$DOKPLOY_WEBHOOK_PDS"

So working my way through it, the when section specifies pushes to main only, and filters on the file changing. This means I only build new images when I change the workflow, or, more importantly, when I bump the version in the file, to match a new tranquil-pds release.

It defines a few dependencies that the nixery server provides for us. I'm self-hosting the server because of reliability issues with the Tangled one. Nixery is this magic tool that builds container images on demand, so that's how we get the environment set up.

Next, we're using kaniko to build the docker image in user space, no socket required, while sidestepping the problem with not having /dev/kvm and not being able to use microvm. The version of Kaniko we want is only distributed as a container image, so I use crane to unpack it. Note that Kaniko wipes the local disk when it runs, but you can protect directories you want to keep around with --ignore-path.

Once built, we have to log into atcr.io to push, passing an app password from the repo secrets. The account that pushes the images lives on the PDS. I override the default distroless image because my server is an ARM machine.

Finally, a webhook hitting my Dokploy instance to get the compose to pull the latest image. And that’s it, the compose owned by Dokploy, everything else on atproto. Take a look at the rest of the repo and the workflows. Most of the setups are more complex than the guides I’ve written, so feel free to copy and adjust.

Thank you for reading

And that’s it! That’s the end of the series. This has been an incredibly fun and educational project, and it’s just made me want to dig deeper into the infrastructure that underpins the ATmosphere and all these incredible services built on top of it.

ps I guess the only thing I’m missing now is a self-hosted component that runs composes for me, backed by on-protocol “deployments” or “stacks”. Would be cool if deploying was just writing to my repo…