Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ on:
push:
branches: [main]

# Cancel outdated runs to save CI minutes
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
checks: write

jobs:
fmt:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -269,3 +278,34 @@ jobs:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build .

docker-umbrel-build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Validate template file exists
run: |
if [ ! -f deployments/umbrel/config.toml.template ]; then
echo "ERROR: config.toml.template is missing"
exit 1
fi
# Verify required placeholders exist
grep -q '\${POSTGRES_PASSWORD}' deployments/umbrel/config.toml.template || (echo "ERROR: POSTGRES_PASSWORD placeholder missing" && exit 1)
grep -q '\${ADMIN_PASSWORD}' deployments/umbrel/config.toml.template || (echo "ERROR: ADMIN_PASSWORD placeholder missing" && exit 1)
grep -q '\${DETECTED_PUBLIC_IP}' deployments/umbrel/config.toml.template || (echo "ERROR: DETECTED_PUBLIC_IP placeholder missing" && exit 1)
grep -q '\${DETECTED_ICANN_DOMAIN}' deployments/umbrel/config.toml.template || (echo "ERROR: DETECTED_ICANN_DOMAIN placeholder missing" && exit 1)
- name: Validate entrypoint script syntax
run: |
if [ ! -f deployments/umbrel/entrypoint.sh ]; then
echo "ERROR: entrypoint.sh is missing"
exit 1
fi
# Validate shell syntax
sh -n deployments/umbrel/entrypoint.sh || (echo "ERROR: entrypoint.sh has syntax errors" && exit 1)
- name: Build Umbrel Docker image
run: docker build -f deployments/umbrel/Dockerfile -t test-umbrel:latest .
- name: Verify required files are in image
run: |
docker run --rm test-umbrel:latest test -f /usr/local/share/config.toml.template || (echo "ERROR: config.toml.template not found in image" && exit 1)
docker run --rm test-umbrel:latest test -x /usr/local/bin/entrypoint.sh || (echo "ERROR: entrypoint.sh not executable" && exit 1)
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ docker run -it pubky:core

Additional optional arguments can be used to run it in the background, but the most important is `--network=host`, which allows the container to access the network and provides an admin endpoint accessible from the host machine. Please refer to the Docker documentation for more detailed options.

#### Umbrel Deployment

For deploying on Umbrel OS, see the [Umbrel deployment guide](./deployments/umbrel/README.md) which includes a specialized Dockerfile and entrypoint script for automatic configuration.

## Links

- [Contributors Guide](./CONTRIBUTORS.md)
81 changes: 81 additions & 0 deletions deployments/umbrel/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# ========================
# Build Stage
# ========================
FROM rust:1.89.0-alpine3.20 AS builder

# TARGETARCH is set by Docker Buildx when using --platform flag
ARG TARGETARCH
RUN echo "TARGETARCH: $TARGETARCH"

# Install build dependencies, including static OpenSSL libraries
RUN apk add --no-cache \
musl-dev \
openssl-dev \
openssl-libs-static \
pkgconfig \
build-base \
curl

# Set cross-compiler environment variables:
# Always set ARM64 variables - safe since unused when targeting x86_64.
# Create environment setup script for x86_64 variables - only when host is ARM so we don't override the native compiler on x86 hosts.
# Set PATH to include both cross-compiler directories (non-existent paths are ignored)
# Set environment variables for static linking with OpenSSL
ENV OPENSSL_STATIC=yes
ENV OPENSSL_LIB_DIR=/usr/lib
ENV OPENSSL_INCLUDE_DIR=/usr/include

# Set the working directory
WORKDIR /usr/src/app

# Copy over Cargo.toml and Cargo.lock for dependency caching
COPY Cargo.toml Cargo.lock ./

# Copy over all the source code
COPY . .

# Build argument for binary selection (always homeserver for Umbrel)
ARG BUILD_TARGET=homeserver

# Build the project in release mode for the MUSL target
# Only apply environment setup script only when host is ARM so we don't override the native compiler on x86 hosts
RUN cargo build --release --bin pubky-$BUILD_TARGET

# Strip the binary to reduce size
RUN strip target/release/pubky-$BUILD_TARGET

# =======================
# Runtime Stage
# =======================
FROM alpine:3.20

ARG TARGETARCH
ARG BUILD_TARGET=homeserver

# Install runtime dependencies (ca-certificates for TLS, wget and netcat for healthchecks, su-exec for user switching, gettext for envsubst)
RUN apk add --no-cache ca-certificates wget netcat-openbsd su-exec gettext

# Create non-root user (use 1000:1000 to match Umbrel's default)
RUN addgroup -g 1000 homeserver && \
adduser -D -u 1000 -G homeserver homeserver

# Copy the compiled binary from the builder stage
COPY --from=builder /usr/src/app/target/release/pubky-$BUILD_TARGET /usr/local/bin/homeserver

# Copy entrypoint script and config template, fix line endings (remove Windows CRLF to ensure Unix LF)
# This ensures the scripts work regardless of the line endings in the source repository
COPY deployments/umbrel/entrypoint.sh /tmp/entrypoint.sh
COPY deployments/umbrel/config.toml.template /usr/local/share/config.toml.template
RUN sed -i 's/\r$//' /tmp/entrypoint.sh && \
mv /tmp/entrypoint.sh /usr/local/bin/entrypoint.sh && \
chmod +x /usr/local/bin/entrypoint.sh && \
chmod 644 /usr/local/share/config.toml.template

# Set the working directory for data
WORKDIR /data

# Expose ports (6286: HTTP, 6287: Pubky TLS, 6288: Admin, 6289: Metrics)
EXPOSE 6286 6287 6288 6289

# Use entrypoint script that handles config creation and user switching
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
107 changes: 107 additions & 0 deletions deployments/umbrel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Umbrel Deployment

This directory contains the Umbrel-specific Docker configuration for the Pubky Homeserver.

## Overview

This deployment configuration is designed specifically for running the Pubky Homeserver on Umbrel. It includes:

- Homeserver-specific Dockerfile with all required dependencies
- Entrypoint script for automatic configuration generation
- User management (non-root user with UID 1000:1000)
- Automatic config.toml generation with sensible defaults

## Files

- `Dockerfile` - Umbrel-specific Dockerfile that builds the homeserver binary
- `entrypoint.sh` - Entrypoint script that handles config generation and user switching

## Building

To build the Umbrel-specific image:

```bash
docker build -f deployments/umbrel/Dockerfile -t pubky-homeserver:umbrel .
```

## Required Environment Variables

The following environment variables are **required** for the homeserver to start:

- `POSTGRES_PASSWORD` - Password for the PostgreSQL database connection
- `ADMIN_PASSWORD` - Password for the admin API

### Optional Environment Variables

- `PUBLIC_IP` - Public IP address of the homeserver (auto-detected if not set)
- `ICANN_DOMAIN` - ICANN domain name for the homeserver (uses `DEVICE_DOMAIN_NAME` if available, otherwise defaults to `localhost`)
- `DEVICE_DOMAIN_NAME` - Umbrel's device domain name (e.g., `umbrel.local`) - used as fallback for `ICANN_DOMAIN`

## Configuration

The entrypoint script automatically generates `/data/config.toml` on first run if it doesn't exist. The configuration includes:

- Database connection using `POSTGRES_PASSWORD`
- Admin API with `ADMIN_PASSWORD`
- Automatic IP/domain detection
- File system storage
- All required services (HTTP, Pubky TLS, Admin, Metrics)

### IP and Domain Detection

The entrypoint script intelligently detects configuration values:

1. **PUBLIC_IP**:
- Uses `PUBLIC_IP` environment variable if set
- Otherwise attempts to auto-detect via `hostname -i`
- Falls back to `127.0.0.1` with a warning

2. **ICANN_DOMAIN**:
- Uses `ICANN_DOMAIN` environment variable if set
- Otherwise uses Umbrel's `DEVICE_DOMAIN_NAME` if available
- Falls back to `localhost` with a warning

**Note:** If defaults (`127.0.0.1`/`localhost`) are used, the homeserver will not be accessible externally. Set `PUBLIC_IP` and `ICANN_DOMAIN` for production deployments.

### External Access and PKARR Configuration

The auto-detection feature attempts to determine the local IP, but this often resolves to the Docker internal IP rather than the public IP needed for PKARR and external services. For production deployments requiring external access, explicitly setting `PUBLIC_IP` and `ICANN_DOMAIN` is recommended.

1. **Set environment variables** in `docker-compose.yml`:
```yaml
homeserver:
environment:
PUBLIC_IP: "your.public.ip.address"
ICANN_DOMAIN: "your-domain.com"
```

2. **Infrastructure setup**:
- Configure NAT traversal (router port forwarding for ports 6286-6289)
- Set up firewall rules to allow incoming connections
- Configure DNS to point your domain to your public IP
- Ensure your network supports the required ports

## Ports

The following ports are exposed:

- `6286` - HTTP (ICANN) endpoint
- `6287` - Pubky TLS endpoint
- `6288` - Admin API
- `6289` - Metrics endpoint

## Security

- Runs as non-root user (UID 1000:1000, matching Umbrel's default)
- `POSTGRES_PASSWORD` should be sourced from Umbrel's secure `APP_PASSWORD` variable
- Each installation gets a unique, securely generated password via Umbrel's framework

## Differences from Root Dockerfile

The root `Dockerfile` is generic and defaults to `testnet`. This Umbrel-specific Dockerfile:

- Always builds the `homeserver` binary
- Includes entrypoint script for config generation
- Creates homeserver user (UID 1000:1000)
- Exposes all required ports
- Includes all runtime dependencies (su-exec, netcat, etc.)
30 changes: 30 additions & 0 deletions deployments/umbrel/config.toml.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[general]
signup_mode = "token_required"
user_storage_quota_mb = 0
database_url = "postgres://pubky:${POSTGRES_PASSWORD}@postgres:5432/pubky_homeserver"

[drive]
pubky_listen_socket = "0.0.0.0:6287"
icann_listen_socket = "0.0.0.0:6286"

[storage]
type = "file_system"

[admin]
enabled = true
listen_socket = "0.0.0.0:6288"
admin_password = "${ADMIN_PASSWORD}"

[metrics]
enabled = true
listen_socket = "0.0.0.0:6289"

[pkdns]
public_ip = "${DETECTED_PUBLIC_IP}"
icann_domain = "${DETECTED_ICANN_DOMAIN}"
user_keys_republisher_interval = 14400
dht_relay_nodes = ["https://pkarr.pubky.app", "https://pkarr.pubky.org"]

[logging]
level = "info"
module_levels = ["pubky_homeserver=info", "tower_http=info"]
88 changes: 88 additions & 0 deletions deployments/umbrel/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/bin/sh
set -e

# Ensure /data directory exists
mkdir -p /data

# Cloudflare Tunnel: read domain from dashboard-written file if present (overrides env)
if [ -f /etc/pubky-cloudflare/domain ] && [ -s /etc/pubky-cloudflare/domain ]; then
CLOUDFLARE_DOMAIN=$(cat /etc/pubky-cloudflare/domain | tr -d '\n\r')
export CLOUDFLARE_DOMAIN
fi

# Generate config.toml if it doesn't exist
if [ ! -f /data/config.toml ]; then
# Validate required environment variables
if [ -z "$POSTGRES_PASSWORD" ]; then
echo "ERROR: POSTGRES_PASSWORD environment variable is required" >&2
exit 1
fi

if [ -z "$ADMIN_PASSWORD" ]; then
echo "ERROR: ADMIN_PASSWORD environment variable is required" >&2
exit 1
fi

# Determine PUBLIC_IP: use env var, or try to detect, or use default
if [ -n "$PUBLIC_IP" ]; then
DETECTED_PUBLIC_IP="$PUBLIC_IP"
else
# Try to get the device's local IP (works in Docker networks)
DETECTED_PUBLIC_IP=$(hostname -i 2>/dev/null | awk '{print $1}' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1)
if [ -z "$DETECTED_PUBLIC_IP" ]; then
DETECTED_PUBLIC_IP="127.0.0.1"
fi
fi

# Determine ICANN_DOMAIN: Cloudflare Tunnel takes precedence, then env, then Umbrel device name, then default
DETECTED_PUBLIC_ICANN_HTTP_PORT=""
if [ -n "$CLOUDFLARE_DOMAIN" ]; then
DETECTED_ICANN_DOMAIN="$CLOUDFLARE_DOMAIN"
DETECTED_PUBLIC_ICANN_HTTP_PORT="443"
elif [ -n "$ICANN_DOMAIN" ]; then
DETECTED_ICANN_DOMAIN="$ICANN_DOMAIN"
elif [ -n "$DEVICE_DOMAIN_NAME" ]; then
DETECTED_ICANN_DOMAIN="$DEVICE_DOMAIN_NAME"
else
DETECTED_ICANN_DOMAIN="localhost"
fi

# Warn if using defaults that won't work for external access (skip when using Cloudflare)
if [ -z "$CLOUDFLARE_DOMAIN" ] && { [ "$DETECTED_PUBLIC_IP" = "127.0.0.1" ] || [ "$DETECTED_ICANN_DOMAIN" = "localhost" ]; }; then
echo "WARNING: Using default values for public_ip ($DETECTED_PUBLIC_IP) and icann_domain ($DETECTED_ICANN_DOMAIN)." >&2
echo "WARNING: Set PUBLIC_IP and ICANN_DOMAIN, or use Cloudflare Tunnel (CLOUDFLARE_DOMAIN + CLOUDFLARE_TUNNEL_TOKEN)." >&2
fi

export POSTGRES_PASSWORD ADMIN_PASSWORD DETECTED_PUBLIC_IP DETECTED_ICANN_DOMAIN
envsubst < /usr/local/share/config.toml.template > /data/config.toml

if [ -n "$DETECTED_PUBLIC_ICANN_HTTP_PORT" ]; then
sed -i "/^icann_domain = /a public_icann_http_port = $DETECTED_PUBLIC_ICANN_HTTP_PORT" /data/config.toml
fi

chmod 644 /data/config.toml || true
chown homeserver:homeserver /data/config.toml || true
fi

# If config already exists and CLOUDFLARE_DOMAIN is set (e.g. from dashboard), update [pkdns]
if [ -f /data/config.toml ] && [ -n "$CLOUDFLARE_DOMAIN" ]; then
if grep -q '^icann_domain = ' /data/config.toml; then
sed -i "s|^icann_domain = .*|icann_domain = \"$CLOUDFLARE_DOMAIN\"|" /data/config.toml
fi
if ! grep -q '^public_icann_http_port = ' /data/config.toml; then
sed -i "/^icann_domain = /a public_icann_http_port = 443" /data/config.toml
else
sed -i 's/^public_icann_http_port = .*/public_icann_http_port = 443/' /data/config.toml
fi
chown homeserver:homeserver /data/config.toml 2>/dev/null || true
fi

# Optimize chown: only run if ownership change is needed
# Check if /data is owned by homeserver user
if [ "$(stat -c '%U:%G' /data 2>/dev/null || stat -f '%Su:%Sg' /data 2>/dev/null)" != "homeserver:homeserver" ]; then
# Only chown if ownership is different
chown -R homeserver:homeserver /data || true
fi

# Switch to homeserver user and run the homeserver with --data-dir /data
exec su-exec homeserver:homeserver homeserver --data-dir /data