# Installation

cronpad is distributed as a container image and as static binaries for Linux and macOS. Every method needs only a persistent local directory for cronpad's files: the two databases and the rebuildable queue file. There is no other service to run. Run exactly one cronpad process per data directory.

## Docker Compose (recommended)

```yaml
# docker-compose.yml
services:
  cronpad:
    image: ghcr.io/gowthamgts/cronpad:1
    ports: ["8080:8080"]
    env_file: .env
    environment:
      CRONPAD_DATABASE__PATH: /data/cronpad.db
      CRONPAD_DATABASE__HISTORY_PATH: /data/history.db
    volumes: ["cronpad-data:/data"]   # also holds the queue, /data/cronpad-queue.db
    restart: unless-stopped

volumes:
  cronpad-data:
```

```sh
docker compose up -d
```

For a local-only demo including a SeaweedFS S3 server, bucket setup and development login, use:

```sh
docker compose --env-file deploy/demo.env -f deploy/docker-compose.yml --profile demo up --build
```

The demo uses fixed local credentials; replace them and configure OAuth for production. Without `LITESTREAM_REPLICA_URL`, cronpad runs [SQLite only](backups.md#sqlite-only-the-default): fine for trying it out, but nothing is replicated.
The repository's `deploy/docker-compose.yml` also has optional profiles: `--profile backup` adds a SeaweedFS S3 server and bucket setup (set the Litestream URL and credentials, or use `deploy/demo.env`), and `--profile observability` adds an OpenTelemetry Collector, Prometheus and Grafana with a ready-made dashboard.

## Docker

```sh
docker run -d --name cronpad -p 8080:8080 \
  -v cronpad-data:/data \
  -e CRONPAD_SERVER__PUBLIC_URL=https://cron.example.com \
  -e CRONPAD_AUTH__GITHUB__CLIENT_ID=… -e CRONPAD_AUTH__GITHUB__CLIENT_SECRET=… \
  -e CRONPAD_SECRETS__ENCRYPTION_KEY=… \
  ghcr.io/gowthamgts/cronpad:1
```

The published image is Alpine-based and includes `/bin/sh` and `curl`; enable the [command sink](../sinks/commands.md) with `CRONPAD_SINKS__COMMAND__ENABLED=true`, and add any other tools your jobs need in your own image.

## Kubernetes

The essentials (a complete example is in `deploy/kubernetes/`):

```yaml
apiVersion: apps/v1
kind: Deployment
metadata: {name: cronpad}
spec:
  replicas: 1                 # exactly one: SQLite has a single writer
  strategy: {type: Recreate}  # never run old and new pods at the same time
  selector: {matchLabels: {app: cronpad}}
  template:
    metadata: {labels: {app: cronpad}}
    spec:
      containers:
        - name: cronpad
          image: ghcr.io/gowthamgts/cronpad:1
          ports: [{containerPort: 8080}]
          envFrom: [{secretRef: {name: cronpad-env}}]
          volumeMounts: [{name: data, mountPath: /data}]
          livenessProbe: {httpGet: {path: /healthz, port: 8080}}
          readinessProbe: {httpGet: {path: /readyz, port: 8080}}
      terminationGracePeriodSeconds: 45   # > server.shutdown_grace
      volumes:
        - name: data
          persistentVolumeClaim: {claimName: cronpad-data}   # ReadWriteOnce, block storage
```

## Binary

```sh
curl -fsSLO https://github.com/gowthamgts/cronpad/releases/latest/download/cronpad_linux_amd64.tar.gz
tar xzf cronpad_linux_amd64.tar.gz
./cronpad serve --config ./cronpad.toml
```

A systemd unit:

```ini
[Unit]
Description=cronpad
After=network-online.target

[Service]
ExecStart=/usr/local/bin/cronpad serve --config /etc/cronpad/cronpad.toml
EnvironmentFile=/etc/cronpad/env
User=cronpad
WorkingDirectory=/var/lib/cronpad
Restart=always
TimeoutStopSec=45

[Install]
WantedBy=multi-user.target
```

## Upgrading from a Redis-based version

Earlier releases used Redis for the queue. Current releases ignore it: on the first start after upgrading, cronpad creates the queue file next to `cronpad.db` (`cronpad-queue.db`) and rebuilds it from your jobs, retries and pending manual runs, so nothing is lost. A leftover `[redis]` section or `CRONPAD_REDIS__*` variable only logs a warning; remove it, then stop the Redis service.

## Behind a reverse proxy

Set `server.public_url` to the external URL and list the proxy's addresses in `server.trusted_proxies` so client IPs are logged and rate-limited correctly. The proxy must pass through `Authorization` headers and allow request bodies up to 1 MiB. No WebSocket or special routing is needed: everything is plain HTTP on one port.

## API-only mode

Set `[ui] dashboard = false` (`CRONPAD_UI__DASHBOARD=false`) to serve only the API, MCP server, sign-in and health endpoints from the same binary.