Run with Docker
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.
Compose
services:
shipyard:
image: shipyard:latest
ports: ["127.0.0.1:8080:8080"]
environment:
SHIPYARD_API_TOKEN: ${SHIPYARD_API_TOKEN:?set a private API token}
volumes:
- ./shipyard.yaml:/config/.shipyard.yaml:ro
- shipyard-data:/data
read_only: true
cap_drop: ["ALL"]
security_opt: ["no-new-privileges:true"]
restart: unless-stopped
healthcheck:
test: ["CMD", "/shipyard", "-healthcheck", "-config", "/config/.shipyard.yaml"]
interval: 30s
timeout: 5s
retries: 3
logging:
driver: json-file
options: {max-size: "10m", max-file: "3"}
volumes:
shipyard-data:
export SHIPYARD_API_TOKEN=$(openssl rand -hex 32)
docker compose up -d
In shipyard.yaml, listen on all interfaces inside the container and keep state on the volume:
listen: 0.0.0.0:8080
storage:
path: /data/shipyard.db
logging:
directory: /data/logs
runtime:
embedded_worker: true
api_token_env: SHIPYARD_API_TOKEN
To 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:
volumes:
- ./config:/config # holds .shipyard.yaml
With the file mounted :ro, the console shows an error when saving a project.
Binding 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.
Health
/shipyard -healthcheck probes the server's /healthz from inside the container — no shell or curl needed. GET /readyz additionally checks the database.
Fleet
Publish 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:
docker run --rm -v ship-state:/var/lib/ship shipyard:latest /ship join -server shipyard.lan:8443 -token SYP1.…
docker run -d -v ship-state:/var/lib/ship --entrypoint /ship shipyard:latest run
Separate worker
To process the queue in its own container, set runtime.embedded_worker: false for the API and start a second service with the same volume:
worker:
image: shipyard:latest
entrypoint: ["/worker"]
command: ["-config", "/config/.shipyard.yaml"]
healthcheck:
test: ["CMD", "/worker", "-healthcheck", "-config", "/config/.shipyard.yaml"]
The worker's health check confirms the queue database answers.