Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
target
**/.git
**/.DS_Store
**/._*
**/.AppleDouble
book
docs
*.md
deploy/README.md
.github
.claude
51 changes: 51 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# syntax=docker/dockerfile:1.7

# Multi-stage rust build for the rota workspace. Produces a slim
# distroless runtime image with both rotad (daemon) and rota (CLI)
# binaries. No shell in the runtime layer; operators talk to a
# running container via `docker exec rota /usr/local/bin/rota ...`.

FROM rust:1.90-slim-bookworm AS build
WORKDIR /src

# System libs the rust toolchain links against: rcgen, ring, rustls,
# instant-acme, pem, x509-parser, sqlite, oneiriq-surql.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
pkg-config \
libssl-dev \
ca-certificates \
clang \
cmake \
protobuf-compiler \
libsqlite3-dev \
&& rm -rf /var/lib/apt/lists/*

# Workspace manifests + source. Cargo.lock is committed so --locked
# gives a reproducible build.
COPY rust-toolchain.toml ./
COPY Cargo.toml Cargo.lock ./
COPY crates ./crates

# Build both binaries in release mode against the pinned toolchain.
RUN cargo build --release --locked --bin rotad --bin rota
RUN strip target/release/rotad target/release/rota || true

# Runtime stage. Distroless cc gives glibc + ca-certs without a shell;
# rotad opens a UNIX socket + an HTTP listener and talks to remote
# CAs / DCV providers, so cc is the right base. Default user is root,
# which keeps file-mode 0600 mounts (config + per-cert keys) readable
# without UID-mapping gymnastics.
FROM gcr.io/distroless/cc-debian12
COPY --from=build /src/target/release/rotad /usr/local/bin/rotad
COPY --from=build /src/target/release/rota /usr/local/bin/rota

# Default config path. Operators bind-mount their rota.yaml here.
ENTRYPOINT ["/usr/local/bin/rotad"]
CMD ["--config", "/etc/rota/rota.yaml"]

# Dashboard / metrics HTTP listener. UNIX control socket lives at
# /var/run/rota.sock (or whatever rota.yaml configures); operators
# bind-mount the socket directory if they want host-side `rota` CLI
# access without `docker exec`.
EXPOSE 7878
131 changes: 131 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Deploying rota

Reference template for standing up a private rota instance from this public repo. The image is generic; your config, credentials, and per-cert private keys live only on your host.

## What's in this directory

* `compose.yml` — operator-private deploy template. Bind-mounts a workdir layout (`rota.yaml`, `secrets/`, `keys/`, `data/`, `run/`) into the container.

The Dockerfile lives at the repo root.

## Public vs private split

Anything in this repo (Dockerfile, compose template, source) is public. Your operator copy of `compose.yml` plus `rota.yaml`, the secrets directory, and the per-cert private keys live ONLY on your deploy host. Never commit them back to this repo.

## Workdir layout

The compose template assumes you run `docker compose up -d` from a workdir that looks like this:

```
/volume1/docker/rota/
├── compose.yml # copied from deploy/compose.yml
├── rota.yaml # your real config (mode 600)
├── secrets/ # mode 700
│ └── namecheap-api.key # mode 600
├── keys/ # mode 700
│ └── example.com.key # mode 600 (rota auto-generates on first renewal)
├── data/ # SQLite audit DB; auto-created
└── run/ # UNIX socket dir; auto-created
```

`/volume1/docker/rota/` is the Synology DSM convention. Adjust as needed for your host.

## Build the image

Build on the same architecture you'll deploy to. Building amd64 images on Apple Silicon via QEMU has been unreliable; build on the deploy host directly when possible.

```bash
# On the deploy host:
git clone https://github.com/Oneiriq/rota /tmp/rota-build
cd /tmp/rota-build
docker build -t ghcr.io/oneiriq/rota:latest .
```

Or build elsewhere and `docker save | ssh host docker load` if your target is resource-constrained.

## Minimal `rota.yaml`

```yaml
daemon:
database_path: /var/lib/rota/rota.db
listen_addr: 0.0.0.0:7878
socket_path: /var/run/rota.sock
check_interval_seconds: 3600
renew_threshold_days: 30

namecheap:
api_key_file: /etc/rota/secrets/namecheap-api.key
username: your-namecheap-username
client_ip: 192.0.2.1 # your deploy host's outbound IP

certs:
- id: example-public
description: example.com marketing site
domains: [example.com, www.example.com]
key_path: /var/lib/rota/keys/example.com.key
ca:
kind: namecheap
ssl_id: 12345678
dcv:
kind: namecheap
install:
kind: filesystem
directory: /var/lib/rota/data/installed
```

`rota.example.yaml` at the repo root has the full annotated reference, including ACME, Cloudflare, webroot, nginx, HAProxy, Kubernetes Secret, alerts, and cluster federation.

## Bring it up

```bash
mkdir -p /volume1/docker/rota/{secrets,keys,data,run}
chmod 700 /volume1/docker/rota/secrets /volume1/docker/rota/keys
cp deploy/compose.yml /volume1/docker/rota/
$EDITOR /volume1/docker/rota/rota.yaml # see template above
$EDITOR /volume1/docker/rota/secrets/namecheap-api.key
chmod 600 /volume1/docker/rota/rota.yaml /volume1/docker/rota/secrets/namecheap-api.key

cd /volume1/docker/rota
docker compose up -d
docker compose logs -f rotad
```

## Use the CLI

The `rota` CLI is bundled in the same image. Talk to the running daemon via `docker exec`:

```bash
docker exec rota /usr/local/bin/rota status
docker exec rota /usr/local/bin/rota renew example-public
docker exec rota /usr/local/bin/rota log example-public
```

If you want host-side CLI access without `docker exec`, the `./run` bind already exposes `/var/run/rota.sock` on the host filesystem. Wire your host's `rota` binary at that socket path.

## Dashboard

The dashboard listens on the port you set in `rota.yaml` (default 7878). The compose template binds to 127.0.0.1 so you can front it with a reverse proxy. For external access put it behind DSM's reverse proxy (or your nginx of choice) with whatever auth you already use.

## Federation

Multi-host federation needs SurrealDB-backed audit and a `cluster:` block. See the [federation runbook](https://oneiriq.github.io/rota/federation.html).

## Upgrades

```bash
cd /volume1/docker/rota
docker compose pull
docker compose up -d
docker image prune -f
```

The audit DB and per-cert private keys persist via the bind mounts; container churn is safe.

## Removing the deployment

```bash
cd /volume1/docker/rota
docker compose down
# data/ and keys/ stay on disk so you can restore later. Delete those
# directories yourself if you really want a clean teardown.
```
37 changes: 37 additions & 0 deletions deploy/compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Reference deploy compose template for a single-node rota instance.
#
# This file is checked into the public rota repo. It does NOT contain
# operator data. To deploy: copy this file plus rota.yaml + secrets +
# private keys into a host workdir (the standard pattern is
# `/volume1/docker/rota/` on Synology), then `docker compose up -d`
# from there. Never commit your operator copy back to this repo.
#
# Expected workdir layout:
#
# ./compose.yml # this file (copied)
# ./rota.yaml # your real config (mode 600)
# ./secrets/ # mode 700; per-vendor API keys mode 600
# ./keys/ # mode 700; per-cert private keys mode 600
# ./data/ # auto-created on first run; SQLite audit DB
# ./run/ # auto-created on first run; UNIX socket dir
#
# CLI access from the host: `docker exec rota /usr/local/bin/rota status`.

services:
rotad:
image: ghcr.io/oneiriq/rota:latest
container_name: rota
restart: unless-stopped
command: ["--config", "/etc/rota/rota.yaml"]
ports:
# Dashboard + /metrics. Bind to 127.0.0.1 if you front this with
# a reverse proxy.
- "${ROTA_DASHBOARD_PORT:-127.0.0.1:7878}:7878"
volumes:
- ./rota.yaml:/etc/rota/rota.yaml:ro
- ./secrets:/etc/rota/secrets:ro
- ./keys:/var/lib/rota/keys
- ./data:/var/lib/rota
- ./run:/var/run
environment:
RUST_LOG: ${RUST_LOG:-info}
Loading