[{"excerpt":"Shipyard is one static binary and one SQLite file. A Raspberry Pi 4 or 5 runs it comfortably.","lang":"","locale":"","tags":["shipyard","raspberry-pi","install"],"taxonomies":{"tag":["shipyard","raspberry-pi","install"]},"text":"Shipyard is one static binary and one SQLite file. A Raspberry Pi 4 or 5 runs it comfortably.\nWhat you need\n\nRaspberry Pi 4 or 5 with 64-bit Raspberry Pi OS or Ubuntu Server\nAn SSD or a good SD card — the queue and logs are written to local disk\nSSH access with sudo\n\n1. Install the binary\nTake shipyard-linux-arm64 from Downloads (shipyard-linux-armv7 on a 32-bit system) and copy it to the Pi:\nscp shipyard-linux-arm64 pi@raspberrypi.local:/tmp/shipyard\nssh pi@raspberrypi.local\nsudo install -m 0755 /tmp/shipyard /usr/local/bin/shipyard\n2. Create the service user\nsudo useradd --system --home-dir /var/lib/shipyard --shell /usr/sbin/nologin shipyard\nsudo install -d -m 0750 -o root -g shipyard /etc/shipyard\n3. Write the configuration\n/etc/shipyard/.shipyard.yaml:\nlisten: 127.0.0.1:8080\nprojects:\n  - id: my-app\n    repository: you/my-app\n    enabled: true\n    issue_label: agent-ready\n    release_interval: 48h\n    content:\n      interval: 168h\n      publish_mode: pull_request\nstorage:\n  path: /var/lib/shipyard/shipyard.db\nlogging:\n  directory: /var/lib/shipyard/logs\nruntime:\n  embedded_worker: true\n  api_token_env: SHIPYARD_API_TOKEN\nobservability:\n  metrics: true\npipelines:\n  project-check:\n    steps:\n      - id: config\n        type: diagnostic\n        timeout: 30s\n4. Set the API token\necho \u0026#34;SHIPYARD_API_TOKEN=$(openssl rand -hex 32)\u0026#34; | sudo tee /etc/shipyard/shipyard.env \u0026gt; /dev/null\nsudo chown root:shipyard /etc/shipyard/shipyard.env /etc/shipyard/.shipyard.yaml\nsudo chmod 0640 /etc/shipyard/shipyard.env /etc/shipyard/.shipyard.yaml\n5. Add the systemd unit\n/etc/systemd/system/shipyard.service:\n[Unit]\nDescription=Shipyard portfolio control plane\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=simple\nUser=shipyard\nGroup=shipyard\nWorkingDirectory=/var/lib/shipyard\nStateDirectory=shipyard\nStateDirectoryMode=0750\nEnvironmentFile=-/etc/shipyard/shipyard.env\nExecStart=/usr/local/bin/shipyard -config /etc/shipyard/.shipyard.yaml\nRestart=on-failure\nRestartSec=5s\nTimeoutStopSec=15s\nSyslogIdentifier=shipyard\nUMask=0077\nNoNewPrivileges=true\nPrivateTmp=true\nProtectSystem=strict\nProtectHome=true\nRestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\n\n[Install]\nWantedBy=multi-user.target\nsudo systemctl daemon-reload\nsudo systemctl enable --now shipyard\n6. Check it\ncurl http://127.0.0.1:8080/healthz\ncurl http://127.0.0.1:8080/readyz\njournalctl -u shipyard -f\n7. Run the first pipeline\nTOKEN=$(sudo sed -n \u0026#39;s/^SHIPYARD_API_TOKEN=//p\u0026#39; /etc/shipyard/shipyard.env)\ncurl -X POST http://127.0.0.1:8080/api/v1/runs \\\n  -H \u0026#34;Authorization: Bearer $TOKEN\u0026#34; \\\n  -H \u0026#39;Content-Type: application/json\u0026#39; \\\n  -H \u0026#39;Idempotency-Key: pi-first-check-001\u0026#39; \\\n  -d \u0026#39;{\u0026#34;project_id\u0026#34;:\u0026#34;my-app\u0026#34;,\u0026#34;kind\u0026#34;:\u0026#34;pipeline\u0026#34;,\u0026#34;task\u0026#34;:\u0026#34;project-check\u0026#34;,\u0026#34;input\u0026#34;:\u0026#34;\u0026#34;}\u0026#39;\nThe run goes through the queue, the embedded worker picks it up and the result is in the run list:\ncurl -H \u0026#34;Authorization: Bearer $TOKEN\u0026#34; \u0026#34;http://127.0.0.1:8080/api/v1/runs?limit=1\u0026#34;\nOpen the UI from your laptop\nShipyard listens on loopback. Tunnel it:\nssh -L 8080:127.0.0.1:8080 pi@raspberrypi.local\nThen open http://127.0.0.1:8080 and paste the token.\nKeep the SD card alive\nCap the journal in /etc/systemd/journald.conf:\nSystemMaxUse=200M\nShipyard's own log and run retention is set under retention: in the configuration.","title":"Install Shipyard on a Raspberry Pi","translation_key":"","url":"/blog/install-shipyard-on-raspberry-pi/"},{"excerpt":"One person can write software for fifty repositories. Keeping fifty repositories maintained, released and explained is a different job. These are the problems…","lang":"","locale":"","tags":["shipyard","operations"],"taxonomies":{"tag":["shipyard","operations"]},"text":"One person can write software for fifty repositories. Keeping fifty repositories maintained, released and explained is a different job. These are the problems Shipyard takes over.\n1. Nobody watches everything\nIssues, failing CI, security alerts and stale pull requests arrive in fifty places. Shipyard collects them into one queue for the repositories you opt in. One screen, one list, sorted by project.\n2. Small fixes never get done\nA dependency bump or a two-line bug fix costs more in context switching than in typing. Label an issue agent-ready and Shipyard turns it into a tested pull request. You review; nothing merges on its own.\n3. Releases depend on memory\nMerged changes sit unreleased because nobody remembers to tag. Shipyard batches approved changes into a release every 48 hours per project. No changes, no release.\n4. Alerts wake you up without context\nAn Alertmanager webhook lands in Shipyard, is matched to its project and starts a diagnostic run. By the time you look, the run log already holds the metrics and the first findings.\n5. Work disappears on restart\nRuns live in a durable SQLite queue with leases and heartbeats. A crashed worker loses nothing: the lease expires and the run is picked up again. Idempotency keys stop the same job from running twice.\n6. Projects go quiet\nSoftware that ships but never explains itself looks abandoned. Once a week Shipyard scores topics for each project and drafts an article when one clears the threshold of 70. Below the threshold it skips the week.\nWhat it costs to run\nOne static binary, one SQLite file, one log directory. It runs on a Raspberry Pi.","title":"The problems Shipyard solves","translation_key":"","url":"/blog/problems-shipyard-solves/"},{"excerpt":"Shipyard runs a software portfolio from one place. It watches the repositories you select, builds approved issues into pull requests and ships releases on a…","lang":"","locale":"","tags":["shipyard"],"taxonomies":{"tag":["shipyard"]},"text":"Shipyard runs a software portfolio from one place. It watches the repositories you select, builds approved issues into pull requests and ships releases on a schedule you set.\nOne Go binary holds the API, the worker and the UI. State lives in SQLite on local disk; every run keeps its own log.\nStart with the pipeline examples, then install it.","title":"Starting Shipyard","translation_key":"","url":"/blog/starting-shipyard/"},{"excerpt":"Queue runs, read their logs, cancel and retry — the same API the console uses.","lang":"","locale":"","tags":null,"taxonomies":{"category":["Documentation"]},"text":"Base URL: the address in listen, e.g. http://127.0.0.1:8080. Requests and responses are JSON; timestamps are Unix seconds; run IDs are 32 hex characters.\nAuthentication\nSend the API token as a bearer token:\n-H \u0026#34;Authorization: Bearer $SHIPYARD_API_TOKEN\u0026#34;\nQueuing, cancelling and retrying always need it. When runtime.api_token_env is set, reads need it too. /healthz and /readyz are always open.\nQueue a run\ncurl -X POST http://127.0.0.1:8080/api/v1/runs \\\n  -H \u0026#34;Authorization: Bearer $SHIPYARD_API_TOKEN\u0026#34; \\\n  -H \u0026#39;Content-Type: application/json\u0026#39; \\\n  -H \u0026#39;Idempotency-Key: nightly-check-2026-10-03\u0026#39; \\\n  -d \u0026#39;{\u0026#34;project_id\u0026#34;:\u0026#34;my-app\u0026#34;,\u0026#34;kind\u0026#34;:\u0026#34;pipeline\u0026#34;,\u0026#34;task\u0026#34;:\u0026#34;project-check\u0026#34;,\u0026#34;input\u0026#34;:\u0026#34;\u0026#34;}\u0026#39;\n\n\n\nField\nMeaning\n\n\n\n\nproject_id\nAn enabled project.\n\n\nkind\ndiagnostic, pipeline or ai_text.\n\n\ntask\nPipeline name for pipeline; AI role for ai_text.\n\n\ninput\nPipeline input or the prompt. Required for ai_text.\n\n\n\nIdempotency-Key (8–128 characters) is required. Sending the same key and body again returns the run already queued instead of a duplicate; the same key with a different body is 409. Answer: 202 with {\u0026quot;data\u0026quot;: run}.\nRuns\n\n\n\nRequest\nReturns\n\n\n\n\nGET /api/v1/runs?limit=50\nNewest runs first, up to 100 per page, and next_cursor.\n\n\nGET /api/v1/runs?before=\u0026lt;cursor\u0026gt;\nThe next page; next_cursor is null on the last one.\n\n\nGET /api/v1/runs/{id}\nOne run.\n\n\nPOST /api/v1/runs/{id}/cancel\nCancels a queued run, asks a running one to stop.\n\n\nPOST /api/v1/runs/{id}/retry\nQueues a failed or cancelled run again as a new run. Needs a new Idempotency-Key.\n\n\n\nA run:\n{\n  \u0026#34;id\u0026#34;: \u0026#34;4bb3a90cb8f0a3bbc59125a2d3a992f8\u0026#34;,\n  \u0026#34;project_id\u0026#34;: \u0026#34;my-app\u0026#34;,\n  \u0026#34;kind\u0026#34;: \u0026#34;pipeline\u0026#34;,\n  \u0026#34;task\u0026#34;: \u0026#34;project-check\u0026#34;,\n  \u0026#34;state\u0026#34;: \u0026#34;succeeded\u0026#34;,\n  \u0026#34;attempt\u0026#34;: 1,\n  \u0026#34;created_at\u0026#34;: 1791057353,\n  \u0026#34;updated_at\u0026#34;: 1791057353,\n  \u0026#34;finished_at\u0026#34;: 1791057353,\n  \u0026#34;logs_expired\u0026#34;: false\n}\nstate is one of queued, running, cancelling, succeeded, failed, cancelled. A failed run carries error.\nLogs\ncurl -H \u0026#34;Authorization: Bearer $SHIPYARD_API_TOKEN\u0026#34; \\\n  \u0026#34;http://127.0.0.1:8080/api/v1/runs/$ID/logs?offset=0\u0026#34;\nReturns up to 100 events and next_offset; pass it back as offset to read on. attempt=N reads an earlier attempt. Expired logs answer 410.\n{\u0026#34;data\u0026#34;: [{\u0026#34;time\u0026#34;: \u0026#34;2026-10-03T18:55:53Z\u0026#34;, \u0026#34;level\u0026#34;: \u0026#34;info\u0026#34;, \u0026#34;run_id\u0026#34;: \u0026#34;4bb3…\u0026#34;, \u0026#34;attempt\u0026#34;: 1, \u0026#34;message\u0026#34;: \u0026#34;run started\u0026#34;}], \u0026#34;next_offset\u0026#34;: 112}\nProjects and pipelines\nGET /api/v1/projects and GET /api/v1/pipelines return the configured projects and pipelines; prompts are never returned. read_only in the projects answer is true when the configuration came from a URL.\nPOST /api/v1/projects adds a project and writes it to the configuration file:\ncurl -X POST http://127.0.0.1:8080/api/v1/projects \\\n  -H \u0026#34;Authorization: Bearer $SHIPYARD_API_TOKEN\u0026#34; \\\n  -d \u0026#39;{\u0026#34;id\u0026#34;:\u0026#34;firmware\u0026#34;,\u0026#34;repository\u0026#34;:\u0026#34;you/firmware\u0026#34;,\u0026#34;enabled\u0026#34;:true,\u0026#34;issue_label\u0026#34;:\u0026#34;agent-ready\u0026#34;,\n       \u0026#34;release_interval\u0026#34;:\u0026#34;48h\u0026#34;,\u0026#34;runs_on\u0026#34;:[\u0026#34;arch=arm64\u0026#34;],\n       \u0026#34;content\u0026#34;:{\u0026#34;enabled\u0026#34;:false,\u0026#34;interval\u0026#34;:\u0026#34;168h\u0026#34;,\u0026#34;min_score\u0026#34;:70}}\u0026#39;\n409 project_exists for a duplicate ID, 409 config_read_only for a URL-loaded configuration, 400 invalid_project with a message naming the problem.\nShips\n\n\n\nRequest\nReturns\n\n\n\n\nGET /api/v1/ships\nShips with labels, platform, online and revoked; enabled tells whether the fleet endpoint is on.\n\n\nPOST /api/v1/ships/tokens\nA one-time join token and the ship join command. Body: {\u0026quot;name\u0026quot;: \u0026quot;garage-pi\u0026quot;, \u0026quot;labels\u0026quot;: [\u0026quot;arch=arm64\u0026quot;]}.\n\n\nPOST /api/v1/ships/{id}/revoke\nRevokes a ship.\n\n\n\nShips themselves talk to Shipyard over gRPC with mutual TLS — see Ships and docks.\nErrors\nErrors have a stable code:\n{\u0026#34;error\u0026#34;: {\u0026#34;code\u0026#34;: \u0026#34;run_not_found\u0026#34;}}\n\n\n\nStatus\nCodes\n\n\n\n\n400\ninvalid_input, invalid_project, invalid_name, invalid_label, invalid_cursor, invalid_attempt, invalid_offset, project_unavailable, pipeline_unavailable, model_or_input_unavailable, unsupported_kind, idempotency_key_required\n\n\n401\nunauthenticated\n\n\n404\nrun_not_found, ship_not_found\n\n\n409\nidempotency_conflict, run_state_conflict, project_exists, config_read_only, fleet_disabled\n\n\n410\nlogs_expired\n\n\n500\nstorage_failed, enqueue_failed, logs_unavailable\n\n\n503\nstorage_unavailable\n\n\n507\nlog_budget_exceeded\n\n\n\nMetrics and the Alertmanager webhook are described in Monitoring.","title":"HTTP API","translation_key":"","url":"/api/"},{"excerpt":"Every key of `.shipyard.yaml`: projects, AI routing, storage, retention, access, monitoring and pipelines.","lang":"","locale":"","tags":null,"taxonomies":{"category":["Documentation"]},"text":"Shipyard reads one YAML file at startup — .shipyard.yaml in the working directory, or the path given with -config. Restart Shipyard after editing it by hand; projects added in the console are written to the file and take effect at once.\n-config also accepts an HTTPS URL. Shipyard downloads the file at startup and treats it as read-only: the console cannot add projects to it. If the server needs a token, put it in SHIPYARD_CONFIG_TOKEN; it is sent as a bearer token.\nSHIPYARD_CONFIG_TOKEN=… shipyard -config https://config.example.com/shipyard.yaml\nUnknown keys, duplicate keys, a second YAML document and files over 1 MiB are rejected at startup. Errors name the offending line.\nMinimal file\nlisten: 127.0.0.1:8080\nprojects:\n  - id: my-app\n    repository: you/my-app\n    enabled: true\n    issue_label: agent-ready\n    release_interval: 48h\n    content:\n      interval: 168h\n      publish_mode: pull_request\nEverything else has a default.\nServer and access\n\n\n\nKey\nDefault\nMeaning\n\n\n\n\nlisten\n—\nAddress and port, e.g. 127.0.0.1:8080.\n\n\nruntime.api_token_env\n—\nName of the environment variable holding the API token.\n\n\nruntime.embedded_worker\nfalse\nRun the queue worker inside the server process.\n\n\n\nWithout a token the console is read-only, and Shipyard only starts on a loopback address. With a token:\n\nqueuing, cancelling and retrying runs require it;\nreading runs, logs, pipelines and /metrics requires it too.\n\nIf api_token_env names a variable that is not set, Shipyard refuses to start.\nProjects\nprojects:\n  - id: my-app\n    repository: you/my-app\n    enabled: true\n    issue_label: agent-ready\n    auto_merge: false\n    release_interval: 48h\n    models:\n      verification: code\n    content:\n      enabled: true\n      interval: 168h\n      min_score: 70\n      target: blog\n      topics: [Go, static sites]\n      publish_mode: pull_request\n\n\n\nKey\nMeaning\n\n\n\n\nid\nUnique ID: letters, digits, ., _, -.\n\n\nrepository\nowner/name.\n\n\nenabled\nOnly enabled projects accept runs.\n\n\nissue_label\nLabel that marks issues ready for automated work.\n\n\nauto_merge\nMust be false: every change goes through review.\n\n\nrelease_interval\nRelease cadence as a duration, e.g. 48h.\n\n\nmodels\nPer-project override of the AI role routing below.\n\n\ncontent.interval\nHow often content is reviewed, e.g. 168h.\n\n\ncontent.min_score\nScore from 0 to 100 a topic needs before an article is drafted.\n\n\ncontent.publish_mode\npull_request: articles are proposed as pull requests.\n\n\nruns_on\nLabels such as arch=arm64; runs go to a ship that carries all of them. See Ships.\n\n\n\nAI providers and models\nai:\n  providers:\n    primary:\n      type: openai-compatible\n      base_url: https://api.openai.com/v1\n      api_key_env: OPENAI_API_KEY\n      timeout: 120s\n    local:\n      type: openai-compatible\n      base_url: http://127.0.0.1:11434/v1\n  models:\n    code:\n      provider: primary\n      model: your-coding-model-id\n      max_output_tokens: 8192\n    researcher:\n      provider: local\n      model: your-local-model-id\n  tasks:\n    coding: code\n    research: researcher\n    writing: code\n    verification: code\n    release_notes: code\n\nProviders — type: openai-compatible works with OpenAI and with local servers that speak the same API (Ollama, vLLM, LM Studio). base_url defaults to OpenAI; it must be HTTPS, or HTTP on loopback. timeout defaults to 120s.\nCredentials — set exactly one of api_key_env (environment variable), api_key_file (file such as /run/secrets/openai_key, up to 64 KiB) or api_key (inline; keep that file private). Local servers need none.\nModels — an alias names a provider and the provider's model ID. max_output_tokens defaults to 4096.\nRoles — tasks maps the roles coding, research, writing, verification and release_notes to model aliases. A project's models overrides them. A role without a model fails the run instead of silently using another one.\n\nKeys are read when a run needs them, so the server starts without AI credentials.\nStorage, logs and retention\n\n\n\nKey\nDefault\nMeaning\n\n\n\n\nstorage.path\n./data/shipyard.db\nSQLite database: queue, history, audit.\n\n\nlogging.directory\n./data/logs\nOne JSONL log per run attempt.\n\n\nlogging.max_run_size_mb\n50\nLog size cap per run.\n\n\nretention.logs_days\n30\nDelete run logs after this many days.\n\n\nretention.completed_runs_days\n90\nDelete finished runs after this many days.\n\n\nretention.audit_days\n365\nDelete audit entries and alert records after this many days.\n\n\nretention.max_disk_size_mb\n2048\nWhen the log directory reaches this size, new runs are refused until retention frees space.\n\n\n\nKeep both paths on a local disk. logs_days cannot exceed completed_runs_days. Details in Monitoring and retention.\nMonitoring and alerts\n\n\n\nKey\nDefault\nMeaning\n\n\n\n\nobservability.metrics\nfalse\nServe Prometheus metrics on /metrics.\n\n\nobservability.otel_endpoint\n—\nOTLP/HTTP collector for traces, e.g. http://127.0.0.1:4318.\n\n\nprometheus.\u0026lt;name\u0026gt;.url\n—\nPrometheus server for prometheus pipeline steps.\n\n\nprometheus.\u0026lt;name\u0026gt;.query\n—\nThe PromQL instant query the step runs.\n\n\nprometheus.\u0026lt;name\u0026gt;.token_env\n—\nOptional bearer token variable for that server.\n\n\nalerts.token_env\n—\nToken variable Alertmanager must send.\n\n\nalerts.project_label\nshipyard_project\nAlert label that names the project.\n\n\nalerts.enqueue_diagnostics\nfalse\nQueue a diagnostic run for each firing alert.\n\n\n\nFleet\n\n\n\nKey\nDefault\nMeaning\n\n\n\n\nfleet.listen\n—\nAddress of the ship endpoint, e.g. 0.0.0.0:8443. Unset means no ships.\n\n\nfleet.address\n—\nhost:port ships dial; used in join commands.\n\n\nfleet.hosts\n—\nDNS names and IPs written into the endpoint's certificate.\n\n\nfleet.directory\n\u0026lt;storage dir\u0026gt;/fleet\nWhere the fleet certificate authority is kept. Back it up: losing it means re-joining every ship.\n\n\n\nThe fleet requires runtime.api_token_env. Setup is described in Ships and docks.\nPipelines\npipelines:\n  project-check:\n    steps:\n      - id: config\n        type: diagnostic\n        timeout: 30s\nA pipeline has 1 to 20 steps with unique IDs. Step types and examples are in Pipelines.\nSecrets in logs\nConfigured API keys and tokens are masked in run logs and errors, together with text that looks like token=…, password: … or an Authorization header. Do not put other secrets into run input.","title":"Configuration","translation_key":"","url":"/configuration/"},{"excerpt":"Connect with your API token, queue runs, follow live logs, cancel and retry.","lang":"","locale":"","tags":null,"taxonomies":{"category":["Documentation"]},"text":"The console is served by Shipyard itself at the address in listen — http://127.0.0.1:8080 by default.\nConnect\nThe pill in the top bar shows your access:\n\n\n\nIndicator\nMeaning\n\n\n\n\nRead-only\nNo token entered. You can browse, but not queue, cancel or retry.\n\n\nConnected\nToken accepted. All actions are available.\n\n\nToken required\nThe server protects reads too. Enter the token to see anything.\n\n\nServer unreachable\nShipyard is not answering.\n\n\n\nClick the pill and paste the value of the variable named by runtime.api_token_env. The token stays in the tab's memory only; reloading the page forgets it.\nRuns\n\nThe Runs view lists the latest runs, newest first, and refreshes every five seconds.\n\nFilter by status — All, Active, Succeeded, Failed, Cancelled — or by project.\nLoad older runs pages back through history.\nClick a run to open its panel: project, type, attempts, timings, the error if it failed, and the log.\n\nQueue a run\n\nNew run opens a form. Pick a project and a type:\n\n\n\nType\nWhat it does\nNeeds\n\n\n\n\nDiagnostic\nChecks that the project is configured and enabled.\nNothing else\n\n\nPipeline\nRuns a pipeline from your configuration.\nA pipeline name; optional input\n\n\nAI text\nSends one prompt to the model routed for the role you pick.\nA role and a prompt\n\n\n\nAI runs call your provider and can cost money.\nFollow the log\nThe log tails live while the run is active; untick Follow to scroll freely. Runs that were retried after a crash have several attempts — switch between them with Attempt.\nCancel and retry\n\nCancel run stops a queued run at once and asks a running one to stop at the next safe point.\nRetry as new run queues a failed or cancelled run again with the same input. The original stays in the history.\n\nProjects\n\nProjects lists the repositories in your configuration with their issue label, release interval, content review and where they run — this Shipyard or ships with matching labels.\nAdd project opens a form: ID, repository, issue label, release interval, optional ship labels and content review. Shipyard validates it, appends it to the configuration file — comments and layout are kept — and starts accepting runs for it immediately, no restart needed. When the configuration is loaded from a URL the view is read-only.\nPipelines\nPipelines shows every pipeline step by step; Run pipeline opens the form with that pipeline selected. Pipelines are defined in the configuration file.\nDocks\n\nDocks lists the ships joined to this Shipyard — online, offline or revoked — with their labels, platform, version and the run they are executing. Add ship creates a one-time join command; Revoke cuts a ship off. The run panel shows where each run ran. See Ships and docks.\nLanguage\nThe console is in English. Choose Polski in the top bar to switch; the choice is remembered in this browser. Add ?lang=pl to a link to open it in Polish.","title":"Using the console","translation_key":"","url":"/console/"},{"excerpt":"Run Shipyard in a container with a persistent volume and a built-in health check.","lang":"","locale":"","tags":null,"taxonomies":{"category":["Documentation"]},"text":"The image is built FROM scratch: the static binary, CA certificates and a default configuration. It runs as an unprivileged user and works with a read-only root filesystem.\nCompose\nservices:\n  shipyard:\n    image: shipyard:latest\n    ports: [\u0026#34;127.0.0.1:8080:8080\u0026#34;]\n    environment:\n      SHIPYARD_API_TOKEN: ${SHIPYARD_API_TOKEN:?set a private API token}\n    volumes:\n      - ./shipyard.yaml:/config/.shipyard.yaml:ro\n      - shipyard-data:/data\n    read_only: true\n    cap_drop: [\u0026#34;ALL\u0026#34;]\n    security_opt: [\u0026#34;no-new-privileges:true\u0026#34;]\n    restart: unless-stopped\n    healthcheck:\n      test: [\u0026#34;CMD\u0026#34;, \u0026#34;/shipyard\u0026#34;, \u0026#34;-healthcheck\u0026#34;, \u0026#34;-config\u0026#34;, \u0026#34;/config/.shipyard.yaml\u0026#34;]\n      interval: 30s\n      timeout: 5s\n      retries: 3\n    logging:\n      driver: json-file\n      options: {max-size: \u0026#34;10m\u0026#34;, max-file: \u0026#34;3\u0026#34;}\n\nvolumes:\n  shipyard-data:\nexport SHIPYARD_API_TOKEN=$(openssl rand -hex 32)\ndocker compose up -d\nIn shipyard.yaml, listen on all interfaces inside the container and keep state on the volume:\nlisten: 0.0.0.0:8080\nstorage:\n  path: /data/shipyard.db\nlogging:\n  directory: /data/logs\nruntime:\n  embedded_worker: true\n  api_token_env: SHIPYARD_API_TOKEN\nTo add projects from the console, mount a writable directory instead of a read-only file — Shipyard replaces the file atomically, which needs write access to its directory:\n    volumes:\n      - ./config:/config            # holds .shipyard.yaml\nWith the file mounted :ro, the console shows an error when saving a project.\nBinding 0.0.0.0 requires api_token_env; Shipyard refuses to start on a non-loopback address without a token. Publish the port on 127.0.0.1 and put a reverse proxy or tunnel in front for remote access.\nHealth\n/shipyard -healthcheck probes the server's /healthz from inside the container — no shell or curl needed. GET /readyz additionally checks the database.\nFleet\nPublish 8443 as well when ships join from other machines, and set the fleet: section — see Ships and docks. The image also contains /ship, so a container can be a ship:\ndocker run --rm -v ship-state:/var/lib/ship shipyard:latest /ship join -server shipyard.lan:8443 -token SYP1.…\ndocker run -d -v ship-state:/var/lib/ship --entrypoint /ship shipyard:latest run\nSeparate worker\nTo process the queue in its own container, set runtime.embedded_worker: false for the API and start a second service with the same volume:\n  worker:\n    image: shipyard:latest\n    entrypoint: [\u0026#34;/worker\u0026#34;]\n    command: [\u0026#34;-config\u0026#34;, \u0026#34;/config/.shipyard.yaml\u0026#34;]\n    healthcheck:\n      test: [\u0026#34;CMD\u0026#34;, \u0026#34;/worker\u0026#34;, \u0026#34;-healthcheck\u0026#34;, \u0026#34;-config\u0026#34;, \u0026#34;/config/.shipyard.yaml\u0026#34;]\nThe worker's health check confirms the queue database answers.","title":"Run with Docker","translation_key":"","url":"/docker/"},{"excerpt":"Install the binary, write a minimal configuration, start the service and queue your first run.","lang":"","locale":"","tags":null,"taxonomies":{"category":["Documentation"]},"text":"1. Install\nDownload the binary for your platform from Downloads and put it on your PATH:\nsudo install -m 0755 shipyard-linux-amd64 /usr/local/bin/shipyard\nOne file holds the API, the worker and the console.\n2. Configure\nCreate .shipyard.yaml in the directory you will run Shipyard from:\nlisten: 127.0.0.1:8080\nprojects:\n  - id: my-app\n    repository: you/my-app\n    enabled: true\n    issue_label: agent-ready\n    release_interval: 48h\n    content:\n      interval: 168h\n      publish_mode: pull_request\nruntime:\n  embedded_worker: true\n  api_token_env: SHIPYARD_API_TOKEN\npipelines:\n  project-check:\n    steps:\n      - id: config\n        type: diagnostic\n        timeout: 30s\nEvery key is described in the configuration reference.\n3. Start\nexport SHIPYARD_API_TOKEN=$(openssl rand -hex 32)\necho \u0026#34;$SHIPYARD_API_TOKEN\u0026#34;   # you will paste this into the console\nshipyard -config .shipyard.yaml\nShipyard creates ./data/shipyard.db and ./data/logs/ on first start. Check it:\ncurl http://127.0.0.1:8080/healthz\n4. Open the console\nOpen http://127.0.0.1:8080, click Read-only in the top bar and paste the token. The indicator turns to Connected.\n5. Queue a run\n\nClick New run, pick your project, choose Pipeline → project-check and Queue run. The run appears at the top of the list; click it to follow its log.\nNext: the console in detail, pipelines, run work on other machines, or run it as a service on a Raspberry Pi.","title":"Getting started","translation_key":"","url":"/getting-started/"},{"excerpt":"Health checks, Prometheus metrics, Alertmanager, traces, service logs and retention.","lang":"","locale":"","tags":null,"taxonomies":{"category":["Documentation"]},"text":"Health\n\n\n\nEndpoint\nAnswers\n\n\n\n\nGET /healthz\n200 while the process serves requests.\n\n\nGET /readyz\n200 when the database answers, 503 otherwise.\n\n\n\nBoth are open without a token. In containers use shipyard -healthcheck — see Run with Docker.\nPrometheus metrics\nSet observability.metrics: true and scrape /metrics:\nscrape_configs:\n  - job_name: shipyard\n    static_configs:\n      - targets: [\u0026#34;127.0.0.1:8080\u0026#34;]\n    authorization:\n      credentials_file: /etc/prometheus/shipyard.token\nshipyard_runs{state=\u0026quot;…\u0026quot;} reports stored runs per state: queued, running, cancelling, succeeded, failed, cancelled. Leave out authorization when no API token is configured.\nAlertmanager\nShipyard receives grouped notifications on POST /webhooks/alertmanager:\n# alertmanager.yml\nreceivers:\n  - name: shipyard\n    webhook_configs:\n      - url: http://127.0.0.1:8080/webhooks/alertmanager\n        http_config:\n          authorization:\n            credentials_file: /etc/alertmanager/shipyard.token\n# .shipyard.yaml\nalerts:\n  token_env: ALERTMANAGER_TOKEN\n  project_label: shipyard_project\n  enqueue_diagnostics: true\nEach alert needs the label shipyard_project with the ID of an enabled project. Shipyard records firing and resolved alerts; with enqueue_diagnostics a firing alert also queues one diagnostic run. A repeated notification for the same alert does not queue a second run. A batch with an unknown project is rejected as a whole and nothing is recorded.\nTraces\nSet observability.otel_endpoint to an OTLP/HTTP collector, e.g. http://127.0.0.1:4318. Shipyard exports a span per run and per pipeline stage. Export failures never block runs.\nService logs\nUnder systemd, Shipyard's own log goes to the journal:\njournalctl -u shipyard -f\njournalctl -u shipyard --since today -o short-iso\nJournal size and retention are host settings (SystemMaxUse in journald.conf). Run logs are separate — they live in logging.directory and follow Shipyard's retention.\nRetention\nShipyard cleans up at worker start and every hour:\n\nrun logs older than retention.logs_days are deleted; the run stays in the history marked logs expired;\nfinished runs older than retention.completed_runs_days are deleted;\naudit entries and alert records older than retention.audit_days are deleted.\n\nQueued and running work is never touched. Each run's log is capped at logging.max_run_size_mb, and single log messages at 16 KiB. When the log directory reaches retention.max_disk_size_mb, new runs are refused with 507 until retention frees space — newer logs are never deleted to make room.\nThe database file itself is not counted against the budget. Back it up with the service stopped, or with sqlite3 shipyard.db \u0026quot;.backup backup.db\u0026quot; while it runs.","title":"Monitoring and retention","translation_key":"","url":"/monitoring/"},{"excerpt":"Ordered steps in YAML: diagnostics, Prometheus queries and AI text.","lang":"","locale":"","tags":null,"taxonomies":{"category":["Documentation"]},"text":"A pipeline is a named list of steps in .shipyard.yaml. Each run goes through the queue, executes the steps in order and writes every step's result to the run log.\n\n\n\nStep type\nDoes\nKeys\n\n\n\n\ndiagnostic\nConfirms the project is configured and enabled.\n—\n\n\nprometheus\nRuns a named PromQL instant query.\nsource\n\n\nai_text\nSends a prompt to the model for a role.\ntask, prompt\n\n\n\nEvery step has an id and an optional timeout (default 2m, at most 15m); a whole run stops after 15 minutes. A failed step stops the pipeline. A pipeline has 1 to 20 steps.\n1. Local project check — no AI credentials\npipelines:\n  project-check:\n    steps:\n      - id: validate-config\n        type: diagnostic\n        timeout: 30s\nConfirms the project's configuration and records the result. Needs no AI credentials.\n2. Prometheus observation\nprometheus:\n  production:\n    url: https://prometheus.example.com\n    token_env: PROMETHEUS_TOKEN\n    query: \u0026#39;up{job=\u0026#34;my-project\u0026#34;}\u0026#39;\npipelines:\n  observe-production:\n    steps:\n      - id: validate-project\n        type: diagnostic\n      - id: collect-metrics\n        type: prometheus\n        source: production\n        timeout: 15s\nThe query result is written to the run log. A failed query, an error status or a response with warnings fails the step.\n3. Article draft with a different reviewer\npipelines:\n  article-draft:\n    steps:\n      - id: write\n        type: ai_text\n        task: writing\n        prompt: \u0026#34;Draft an article about ${input}. Use only supplied facts; list missing evidence.\u0026#34;\n        timeout: 2m\n      - id: review\n        type: ai_text\n        task: verification\n        prompt: \u0026#34;Review this draft and return a corrected version: ${previous}\u0026#34;\n        timeout: 2m\nwriting and verification resolve through ai.tasks and the project's models override, so the draft and the review can use different models. ${input} is the run input; ${previous} is the previous step's output. They are filled once — placeholder text inside the input is passed on as written. Prompts go to the provider as text, never to a shell. AI calls can cost money.\nRun a pipeline\nIn the console: New run → Pipeline → pick the pipeline and project. Or from the Pipelines view, Run pipeline. API equivalent:\ncurl -X POST http://127.0.0.1:8080/api/v1/runs \\\n  -H \u0026#34;Authorization: Bearer $SHIPYARD_API_TOKEN\u0026#34; \\\n  -H \u0026#39;Content-Type: application/json\u0026#39; \\\n  -H \u0026#39;Idempotency-Key: my-project-check-001\u0026#39; \\\n  -d \u0026#39;{\u0026#34;project_id\u0026#34;:\u0026#34;ssg\u0026#34;,\u0026#34;kind\u0026#34;:\u0026#34;pipeline\u0026#34;,\u0026#34;task\u0026#34;:\u0026#34;project-check\u0026#34;,\u0026#34;input\u0026#34;:\u0026#34;\u0026#34;}\u0026#39;\nReusing the idempotency key returns the run already queued. One run per project executes at a time; others wait in the queue.\nIf Shipyard stops mid-run, diagnostic runs are picked up again (up to three attempts). Pipeline and AI runs are marked failed instead, so a paid model call is never repeated without you — use Retry.\nThe log records each stage's start and output and ends with pipeline completed. Messages over 16 KiB are truncated.","title":"Pipelines","translation_key":"","url":"/pipelines/"},{"excerpt":"Ubuntu, Raspberry Pi, FreeBSD and macOS — one static binary per platform.","lang":"","locale":"","tags":null,"taxonomies":{"category":["Documentation"]},"text":"Platform\nArchitecture\nFile\n\n\n\n\nUbuntu Linux\namd64\nshipyard-linux-amd64\n\n\nUbuntu Linux, Raspberry Pi 4/5 (64-bit OS)\narm64\nshipyard-linux-arm64\n\n\nRaspberry Pi 4 (32-bit OS)\narmv7\nshipyard-linux-armv7\n\n\nFreeBSD\namd64\nshipyard-freebsd-amd64\n\n\nFreeBSD\narm64\nshipyard-freebsd-arm64\n\n\nmacOS, Apple Silicon\narm64\nshipyard-darwin-arm64\n\n\n\nEach binary is statically linked: no C library, interpreter or database server to install. The console is built in. Get them from Downloads.\nRunning as a service\n\nLinux — systemd; the Raspberry Pi guide has a complete unit file that works on any systemd distribution.\nContainers — see Run with Docker.\nFreeBSD and macOS — run the binary under your service manager (rc.d, launchd) with -config pointing at your configuration.\n\nKeep storage.path and logging.directory on a local disk; SQLite should not sit on a network share.","title":"Platforms","translation_key":"","url":"/platforms/"},{"excerpt":"Run work on other machines: install a ship, join it with a one-time token and route projects to it with labels.","lang":"","locale":"","tags":null,"taxonomies":{"category":["Documentation"]},"text":"A ship is a worker on another machine — a Raspberry Pi next to the hardware it tests, a build box with more cores, a FreeBSD host. Ships connect to Shipyard, lease runs, execute them locally and send logs and results back. Docks in the console lists every ship.\nHow it works\n\nShipyard opens a fleet endpoint (gRPC over TLS, port 8443 by default) with its own certificate authority.\nYou create a one-time join token in Docks. It is valid for one hour and carries the CA fingerprint, so the ship verifies it is talking to your Shipyard.\nship join exchanges the token for the ship's own client certificate. From then on every call uses mutual TLS.\nA project with runs_on labels runs only on a ship that carries all of them. Projects without runs_on stay on the Shipyard machine.\nCredentials never travel: a run tells the ship which environment variable or file holds a key, and the ship reads it locally.\n\n1. Open the fleet endpoint\nAdd to Shipyard's configuration and restart:\nruntime:\n  api_token_env: SHIPYARD_API_TOKEN\nfleet:\n  listen: 0.0.0.0:8443\n  address: shipyard.lan:8443          # what ships dial\n  hosts: [shipyard.lan, 192.168.1.10] # names and IPs in the server certificate\nShips must reach address. Open TCP 8443 to them in your firewall. With Docker, publish 8443 next to 8080.\n2. Route a project to ships\nprojects:\n  - id: firmware\n    repository: you/firmware\n    enabled: true\n    runs_on: [arch=arm64, site=garage]\n    # …\nRuns of firmware wait in the queue until a ship with both labels is free.\n3. Install the ship\nDownload ship for the machine's platform from Downloads:\nsudo install -m 0755 ship-linux-arm64 /usr/local/bin/ship\nsudo useradd --system --home-dir /var/lib/ship --shell /usr/sbin/nologin ship\nsudo install -d -m 0700 -o ship -g ship /var/lib/ship\n4. Join\nIn the console open Docks → Add ship, give it a name and labels, and copy the command.\n\nRun it on the new machine as the ship user:\nsudo -u ship ship join -server shipyard.lan:8443 -token SYP1.… -name garage-pi\nThe ship writes its identity, private key and certificate to /var/lib/ship. The private key never leaves the machine. A token works once; create a new one for every ship.\n5. Run it as a service\n/etc/systemd/system/ship.service:\n[Unit]\nDescription=Shipyard ship\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nUser=ship\nGroup=ship\nStateDirectory=ship\nStateDirectoryMode=0700\nEnvironmentFile=-/etc/ship/ship.env\nExecStart=/usr/local/bin/ship run -state /var/lib/ship\nRestart=always\nRestartSec=5s\nUMask=0077\nNoNewPrivileges=true\nProtectSystem=strict\nProtectHome=true\n\n[Install]\nWantedBy=multi-user.target\nsudo systemctl daemon-reload\nsudo systemctl enable --now ship\njournalctl -u ship -f\nPut the provider keys this ship's runs need into /etc/ship/ship.env (mode 0600), e.g. OPENAI_API_KEY=…. A local model server on the ship (Ollama, vLLM) needs no key at all.\nDocks\n\n\n\n\nColumn\nShows\n\n\n\n\nStatus\nOnline — called in within 15 seconds. Offline — not heard from. Revoked — no longer allowed.\n\n\nLabels\nWhat the ship carries for routing.\n\n\nPlatform, version\nReported by the ship.\n\n\nRunning\nThe run it is executing now, or Idle.\n\n\n\nRevoke cuts a ship off immediately: its certificate stops working, and a run it was executing is recovered like any interrupted run. To bring the machine back, join it again with a new token.\nThe run panel shows Ran on — the ship, or the Shipyard machine — for every run.\nCommands\nship join -server HOST:PORT -token TOKEN [-name NAME] [-label key=value]... [-state DIR]\nship run [-state DIR]\nship version\n-state defaults to /var/lib/ship. Labels given with -label are added to those in the token.","title":"Ships and docks","translation_key":"","url":"/ships/"},{"excerpt":"Guides and notes on running Shipyard: what it solves, how to install it and how to operate a software portfolio with it.","lang":"","locale":"","tags":null,"text":"","title":"Blog","translation_key":"","url":"/blog/"},{"excerpt":"Shipyard 1.0.0 for Linux, Raspberry Pi, FreeBSD and macOS. Released 14 May 2026; download files are in testing.","lang":"","locale":"","tags":null,"text":"Version 1.0.0 — released 14 May 2026.\nFiles in testing.\n\n\n\nPlatform\nArchitecture\nShipyard\nShip\nStatus\n\n\n\n\nUbuntu Linux\namd64\nshipyard-linux-amd64\nship-linux-amd64\nIn testing\n\n\nUbuntu Linux, Raspberry Pi 4/5 (64-bit)\narm64\nshipyard-linux-arm64\nship-linux-arm64\nIn testing\n\n\nRaspberry Pi 4 (32-bit)\narmv7\nshipyard-linux-armv7\nship-linux-armv7\nIn testing\n\n\nFreeBSD\namd64\nshipyard-freebsd-amd64\nship-freebsd-amd64\nIn testing\n\n\nFreeBSD\narm64\nshipyard-freebsd-arm64\nship-freebsd-arm64\nIn testing\n\n\nmacOS, Apple Silicon\narm64\nshipyard-darwin-arm64\nship-darwin-arm64\nIn testing\n\n\n\nEach platform has two files:\n\nshipyard-* — the control plane: API, console and worker in one binary.\nship-* — the remote worker that joins a Shipyard from another machine. See Ships and docks.\n\nBoth are static binaries with no runtime dependencies.\nNext: install on a Raspberry Pi or read the configuration guide.","title":"Downloads","translation_key":"","url":"/downloads/"}]