# 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

```yaml
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:
```

```sh
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:

```yaml
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:

```yaml
    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](/ships/). The image also contains `/ship`, so a container can be a ship:

```sh
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:

```yaml
  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.
