src/services/buildEngine.ts and runs entirely on the server — no external CI service required. Each stage writes progress back to your browser via Socket.io so you can watch the build unfold in real time.
Build status values
A build record moves through these statuses during the pipeline:| Status | Meaning |
|---|---|
queued | Build record created, pipeline not yet started |
running | Pipeline is actively executing |
passed | All stages completed with exit code 0 |
failed | Any stage exited with a non-zero code |
Pipeline stages
Webhook received and verified
GitHub sends a
POST to /api/webhook for every push to a watched branch. The server computes an HMAC-SHA256 digest of the raw request body using WEBHOOK_SECRET and compares it to the X-Hub-Signature-256 header using a timing-safe comparison. Requests with invalid signatures are rejected before any build work begins.Build record created
A row is inserted into the
build table with status: "queued". The commit message, commit hash, branch, and author are captured from the webhook payload. A buildStatusUpdate event is emitted over Socket.io to notify the client that the build is starting.Temp directory created
A working directory is created at:The timestamp component (
Date.now()) ensures concurrent builds for different projects never collide.Secrets decrypted and written to .env
If the project has any stored secrets, each value is decrypted with AES-256-GCM and written to a The presence of this file is tracked with a
.env file inside the build directory:hasEnvFile flag that controls both the --env-file argument to docker run and the deployment decision at the end of the pipeline.Repository cloned
The GitHub access token stored on the user record is embedded in the clone URL to allow access to private repositories:The shallow clone (
--depth 1) keeps the operation fast by avoiding full history.Specific commit fetched and checked out
After the initial clone, the exact commit hash from the webhook payload is fetched and checked out. This guarantees the build always runs against the precise commit that triggered the webhook, even if newer commits have landed since:
Framework detection
Shipyard inspects the root of the cloned repository for well-known framework config files and sets the output directory automatically:
| Config file detected | Output directory |
|---|---|
vite.config.ts or vite.config.js | dist |
next.config.ts or next.config.js | .next |
| Neither (or a manually configured value) | Project’s outputDirectory setting |
Dockerfile generated if none exists
If the repository does not include a The
Dockerfile, Shipyard generates one at <buildPath>/Dockerfile:CMD line is set to the project’s configured build command. Projects that already ship a Dockerfile use it as-is.Docker image built
The image tag is derived from the project name (lowercased, spaces replaced with hyphens, special characters removed):Standard output and standard error from the Docker daemon are both captured. Lines from stderr are inspected for keywords (
error, failed, fatal, exception) to determine whether each line is an actual error or informational Docker progress output. All lines are emitted to the client via build_logs / build_errors Socket.io events.Container executed with volume mount
After a successful image build, a container is run with the build directory mounted so output written inside The
/app is immediately visible on the host:--user flag passes the server process’s own UID and GID into the container. This prevents files written by the container from being owned by root on the host, which would block subsequent cleanup. The --env-file argument is only added when the project has secrets.Output from this stage is emitted via run_logs / run_error Socket.io events.Build logs batched and saved
Log lines are accumulated in an in-memory buffer as they arrive from
docker build and docker run. When each process closes, the entire buffer is flushed to the build_logs table in a single db.insert() call. Each row stores the line number, raw log text, and the build ID.Build status updated
Once the container exits, the
build row is updated:- Exit code 0 →
status: "passed",finishedAtset to now - Any other exit code →
status: "failed",finishedAtset to now
buildStatusUpdate event is emitted to the client reflecting the final status.Deployment triggered (if eligible)
If the build passed and the project has no secrets (i.e.
hasEnvFile is false), deployProject is called automatically. See Static site deployment and subdomain routing for details on what happens next.Projects with environment variables are not automatically deployed as static sites. Because a static site is served directly from the filesystem, any secrets embedded in the build output would be publicly accessible. Keep secrets-based projects behind a server-side runtime that can read them safely.