Hosting your own instance

I see that there is a Docker container in the root of the code on tangled.

I want to get things running for music projects in Vancouver, so I grabbed the source from tangled and pushed it up in a new repo, and then connected that repo to Railway in the hopes that it would auto-build :stuck_out_tongue:

Got this error message

And of course, rather than me flailing about, got any pointers on what I need to do to self host?

Got this message:

I’ll update the Dockerfile to include cache IDs. I haven’t run into that before, but a quick Google search recommends it for some CI/CD platforms.

I’ll add this to the build instructions, but I’ve found that I sometimes need to run cargo build outside of docker because a library could update in between when the lock file is generated vs when the build checks dependencies inside of the docker build.

Also, this it the command that I’m using to build production builds of Smoke Signal:

docker build --progress=plain --pull --platform=linux/amd64 --pull -t smokesignal:`date +%s` .

Changes pushed. See 2912bb5.

Can you pull and try again?

OK I did some brutal rebase hackery.

Different error!

Cache mount ID is not prefixed with cache key

Probably this needs to be done successfully locally and then move on to getting it working on Railway.

I do think getting it working on Railway, plus a HOWTO install on Digital Ocean would be the two targets I would aim for initially.

And actually, ahead of that, a HOWTO on local development would be good!

Note: @ngerakines has this running on Railway now, so we’ll get this documented. I think he might also make a Railway template, like the PDS one that exists.

I’ll be hosting it on a docker of mine in a datacenter.

Just so i understand, right now, docker-compose.yml is spinning the infra and you have to spin the smokesignal docker manually right ?

  • git clone https://tangled.sh/@smokesignal.events/smokesignal
  • cd smokesignal/
  • docker compose up -d
  • docker run -d --name valkey valkey/valkey:8-alpine
  • docker build --progress=plain --pull --platform=linux/amd64 --pull -t smokesignal:'date +%s' .
  • docker run -d --name smokesignal --env-file=.env -p 3100:3100 smokesignal:'date +%s'

.env :

DEBUG=true
HTTP_PORT=3100
DNS_NAMESERVERS=1.1.1.1
PLC_HOSTNAME=plc.directory
EXTERNAL_BASE= [hostname]
HTTP_COOKIE_KEY= [random hexkey]
OAUTH_ACTIVE_KEYS=[random hexkey]
DESTINATION_KEY=[random hexkey]
SIGNING_KEYS=[random hexkey]
RUST_LOG=smokesignal=debug,html5ever=info,info
ADMIN_DIDS=

Am i going offroad ?

EDIT: i tried but am failing in the keys section. used the dev ones but i suspect they are no good for another deployment (and not random :sweat_smile: ).

minio and postgres are ok, createbuckets rolled once.

Oh, i found my answers, but in CLAUDE.MD heh.

Configuration Requirements

Required Environment Variables

  • HTTP_COOKIE_KEY - 64-character hex key for session encryption
  • DATABASE_URL - PostgreSQL connection string
  • REDIS_URL - Redis/Valkey connection string
  • EXTERNAL_BASE - Public base URL (e.g., https://smokesignal.events)
  • PLC_HOSTNAME - AT Protocol PLC server hostname
  • ADMIN_DIDS - Comma-separated admin user DIDs

OAuth Backend Configuration

  • OAUTH_BACKEND - OAuth backend to use: ā€œatprotocolā€ (default) or ā€œaipā€

When OAUTH_BACKEND=atprotocol, the following variable is required:

  • SIGNING_KEYS - Path to JWK key set file for OAuth signing

When OAUTH_BACKEND=aip, the following variables are required:

  • AIP_HOSTNAME - AIP OAuth server hostname
  • AIP_CLIENT_ID - AIP OAuth client ID
  • AIP_CLIENT_SECRET - AIP OAuth client secret

Optional Environment Variables

  • RUST_LOG - Logging configuration (default: info)
  • PORT - Server port (default: 3000)
  • BIND_ADDR - Bind address (default: 0.0.0.0)

Looks like it also miss redis/valkey in the docker-compose file

  valkey:
    image: valkey/valkey:8-alpine

I’m now stuck at the keys.json.

i generated a new file but where do i put it ?

Hmm, that looks a bit outdated. The value of the signing keys environment variable is a ';' separated list of did-key values.

Example:

SIGNING_KEYS=did:key:z42tmnvX4o8ernfo1UvqBEXZ3EWNxp1nmSHBPRDB3wqBUBQz

The above value was generated with goat (don’t use it).

goat key generate -t p256
Key Type: P-256 / secp256r1 / ES256 private key
Secret Key (Multibase Syntax): save this securely (eg, add to password manager)
        z42tmnvX4o8ernfo1UvqBEXZ3EWNxp1nmSHBPRDB3wqBUBQz
Public Key (DID Key Syntax): share or publish this (eg, in DID document)
        did:key:zDnaekQ4zRLMmXqSa7NZZfHxXWZ1xbcnjQD5BJ38fSrJKTiC8
1 Like

Almost !

Error: error-config-1 CONTENT_STORAGE must be set

Ok. got it

How i made it :

  • install GOAT
  • goat key generate -t p256
  • git clone https://tangled.sh/@smokesignal.events/smokesignal
  • cd smokesignal/
  • docker compose up -d
  • docker run -d --name valkey valkey/valkey:8-alpine
  • docker build --progress=plain --pull --platform=linux/amd64 --pull -t smokesignal:'date +%s' .
  • docker run -d --name smokesignal -v /etc/smokesignal/content/:/content:rw --network=smokesignal_network --env-file=.env -p 3100:3100 smokesignal:'date +%s'

.env

DEBUG=true
HTTP_PORT=3100
PLC_HOSTNAME=plc.directory
EXTERNAL_BASE= [host]
HTTP_COOKIE_KEY= [ We have to do **cargo run --bin crypto -- key** on dev ? ]
SIGNING_KEYS= [Generated above]
DATABASE_URL=postgres://smokesignal:[changeme]@postgres/smokesignal_dev
REDIS_URL=redis://valkey:6379/0
RUST_LOG=smokesignal=debug,html5ever=info,info
ADMIN_DIDS=did:plc:something
CONTENT_STORAGE=/content

It works but cannot login.

  2025-07-15T20:47:58.293463Z  INFO atproto_jetstream::consumer: Starting Jetstream consumer
    at /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atproto-jetstream-0.9.7/src/consumer.rs:284
  2025-07-15T20:48:02.637401Z DEBUG smokesignal::http::middleware_i18n: Using default language, language: en-US
    at src/http/middleware_i18n.rs:186
  2025-07-15T20:48:07.107327Z DEBUG smokesignal::http::middleware_i18n: Using default language, language: en-US
    in tower_http::trace::make_span::request with method: GET, uri: /oauth/login, version: HTTP/1.1
    in tower_http::trace::make_span::request with method: POST, uri: /oauth/login, version: HTTP/1.1
  2025-07-15T20:48:07.612625Z  WARN smokesignal::http::handle_oauth_login: encountered error, error: PARHttpRequestFailed(Middleware(error-atproto-oauth-dpop-1 Unexpected OAuth error: invalid_request
Stack backtrace:
   0: <unknown>

Followup next morning.

I was chattin with nathalie.sh yesterday as she was settingup another stream.place instance and it made me think about reverse-proxy.

I use Zoraxy (written in go) and i didn’t put any special header forwarding.

I think it is (or a part) of the problem. I’ll check if i add the same one i use with Discourse.

I’ve been using https://tunnelto.dev, but it’s OK. It sounds like the PDS isn’t able to complete the GET request to the client metadata.

I use it too for dev, but this instance is ā€œpublicā€ as it sits on my hosted server on https://plaquetournante.art .

I disable the reverse proxying when not testing but it’s live right now.

When testing /oauth/par i get no response.

curl -X POST http://[local adress behind proxy]:3100/oauth/par -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=test&response_type=code"

Just to take a step back, the ATProtocol OAuth process looks like this:

App provides GET /oauth/client-metadata as a publicly available endpoint.

  1. User goes to log into App.
  2. App accepts the login_hint and resolves the DID document for it, gets the PDS, and then makes a request to PDS/.well-known/oauth-protected-resource, then gets the authorization_servers value from that, then makes a request to authorization_server/.well-known/oauth-authorization-server to get the par endpoint and authorization endpoint.
  3. App generates a DPoP JWK used for this OAuth session
  4. App makes a request to the PAR endpoint with the client assertion and DPoP JWK, then gets back a request-uri
  5. App directs the user to token_endpoint?request-uri=xyz
  6. User authenticates in their PDS and then are returned to App/oauth/callback?state=x&code=y
  7. App looks up oauth request by state and gets the authorization server, then requests authorization server meta-data to extract token endpoint
  8. App makes DPoP’d call to token endpoint with state, code, and params* from PAR and server responds with access_token, refresh_token, and expires.
  9. App can now make DPoP’d XRPC calls to PDS using access_token and DPoP JWK

I’m not sure what you’re describing as the OAuth PAR endpoint with your example curl call.

Looks like my DNS is time-outing.

  2025-07-16T15:44:01.495597Z ERROR atproto_identity::resolve: error: error-atproto-identity-resolve-6 Invalid HTTP resolution response: expected DID format
  2025-07-16T15:44:16.394579Z  WARN smokesignal::http::handle_oauth_login: encountered error, error: error-atproto-identity-resolve-2 No DIDs resolved for handle: no resolution methods succeeded
    at /usr/local/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/atproto-identity-0.9.7/src/resolve.rs:83
    in atproto_identity::resolve::resolve_handle_http with handle: "ngerakines.me"
    in atproto_identity::resolve::resolve_handle with handle: "ngerakines.me"
    in atproto_identity::resolve::resolve_subject with subject: "ngerakines.me"
    in tower_http::trace::make_span::request with method: POST, uri: /oauth/login, version: HTTP/1.1
  2025-07-16T15:44:16.394475Z ERROR atproto_identity::resolve: error: error-atproto-identity-resolve-4 DNS resolution failed: ResolveError { kind: Proto(ProtoError { kind: Timeout }) }
    in atproto_identity::resolve::resolve_handle_dns with lookup_dns: "ngerakines.me"
  2025-07-16T15:44:16.394492Z ERROR atproto_identity::resolve: error: error-atproto-identity-resolve-2 No DIDs resolved for handle: no resolution methods succeeded
  2025-07-16T15:44:16.394500Z ERROR atproto_identity::resolve: error: error-atproto-identity-resolve-2 No DIDs resolved for handle: no resolution methods succeeded

and this is when i try with my did directly


  2025-07-16T18:09:24.314048Z  WARN smokesignal::http::handle_oauth_login: encountered error, error: PARHttpRequestFailed(Middleware(error-atproto-oauth-dpop-1 Unexpected OAuth error: invalid_request