Skip to content

Latest commit

 

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Wharf

CI CodeQL Go report card Go 1.25 MIT

Wharf turns a single machine with a container runtime into a place you can deploy to. Add it as a git remote, push, and it builds an image from your Dockerfile, starts the new version alongside the old one, waits for it to answer its health check, moves traffic across, and stops what was there before. If the new version never becomes healthy, nothing moves and the old release keeps serving.

Two binaries: wharf runs the platform, wharfctl drives it. It talks to the container runtime over its own socket, so the only dependency outside the standard library is a YAML parser and the Prometheus client.

A deployment, start to finish

$ wharfctl create api -port 3000 -replicas 2 -env DATABASE_URL=postgres://db/api
created api

  git remote add wharf http://wharf.example.com:8081/git/api
  git push wharf main

It will serve on api.wharf.local once the first push succeeds.

$ git push wharf main
   036c765..eac7b1d  main -> main

$ wharfctl follow api-2
13:30:24 build    Step 1/10 : FROM golang:1.25-alpine AS build
13:30:31 build    Successfully tagged wharf/api:eac7b1d
13:30:31 build    image ready in 7.489s
13:30:31 release  started wharf-api-2-1 at 192.168.0.3
13:30:31 release  started wharf-api-2-2 at 192.168.0.5
13:30:31 release  waiting for wharf-api-2-1 to answer on /healthz
13:30:32 release  wharf-api-2-1 is healthy
13:30:32 wharf    traffic now serves api-2 on api.wharf.local
13:30:32 wharf    retiring api-1
13:30:32 wharf    deployment complete

The push returns as soon as the objects are stored; the build runs behind it. Nothing about the running application changes until the new instances answer.

What happens between push and traffic

sequenceDiagram
    participant D as Developer
    participant W as Wharf
    participant R as Container runtime
    participant U as Users

    D->>W: git push wharf main
    W->>W: store objects, read the commit
    W-->>D: push accepted
    W->>W: export the revision to a build context
    W->>R: build the image from the Dockerfile
    W->>R: start the new instances
    W->>R: poll the health path until it answers
    Note over W,R: the old release is still serving
    W->>W: switch the routing table
    U->>W: request
    W->>R: proxied to the new release
    W->>R: stop the old instances
Loading

Every step writes to a log stream that the dashboard and wharfctl follow read live, and that is stored with the release so it is still there tomorrow.

Running it

Wharf itself runs in a container with the runtime socket mounted:

services:
  wharf:
    image: wharf:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - wharf-state:/var/lib/wharf
    ports:
      - "80:8080"     # traffic to the applications
      - "8081:8081"   # git remote, API and dashboard

Or clone and run the whole thing locally, which is what the screenshots come from:

git clone https://github.com/cansarihan/wharf.git
cd wharf
make demo          # control plane on http://127.0.0.1:18091
make demo-down

Applications are reached by hostname. api is served at api.wharf.local by default; point a wildcard DNS record or a /etc/hosts entry at the machine, or give an application its own domain with -domain.

The control plane

Applications are listed as berths: what is deployed, which commit, how each instance is doing, and what to do next. Selecting one shows its pipeline and its live build log.

Wharf control plane

The same information reaches a terminal:

$ wharfctl apps
NAME     HOST                 RELEASE   PHASE   INSTANCES   REPLICAS   DEPLOYS   UPDATED
api      api.wharf.local      eac7b1d   live          2/2          2         4   2m ago
demo     demo.wharf.local     145d769   live          2/2          2         5   1m ago
worker   worker.wharf.local   eac7b1d   live          2/2          2         1   8m ago

$ wharfctl app api
api  api.wharf.local
git remote     http://127.0.0.1:8081/git/api
replicas       2
release        api-2 eac7b1d live
commit         feat: mark the response as version two by An Author
built in       7.5 s

INSTANCE        ADDRESS        STATUS    CPU     MEMORY   STARTED
wharf-api-2-1   192.168.0.3    running   1.7%    8 MB     2m ago
wharf-api-2-2   192.168.0.5    running   1.2%    8 MB     2m ago

Commands

Command What it does
wharfctl create <name> Create an application and its git remote
wharfctl apps, app <name> List applications, or inspect one with its instances
wharfctl releases <name> Release history, newest first, with build times and failures
wharfctl follow <release> Stream a build and release log as it happens
wharfctl logs <name> Recent output from the running instances
wharfctl scale <name> <n> Change the replica count for the next deployment
wharfctl env <name> KEY=VALUE Set variables, KEY- removes one
wharfctl rollback <name> Bring the previous release back
wharfctl stop <name> Stop the instances without deleting anything
wharfctl destroy <name> Remove the application, its instances and its repository
wharfctl routes, status What is published, and how the platform is doing

Everything is also an HTTP API on the control port: /api/v1/apps, /api/v1/apps/{name}/releases, /api/v1/releases/{id}/stream for server sent events, POST /api/v1/apps/{name}/rollback, and the rest. Setting server.admin_token requires a bearer token on all of them except /healthz.

What an application needs

A Dockerfile and an HTTP server that listens on $PORT:

FROM golang:1.25-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/api .

FROM alpine:3.21
COPY --from=build /out/api /usr/local/bin/api
ENTRYPOINT ["/usr/local/bin/api"]

Three variables are always present in the container: PORT, WHARF_APP and WHARF_RELEASE. Anything set with wharfctl env joins them, and variables named BUILD_* are also passed to the image build as build arguments, which keeps deployment secrets out of image layers.

The health path is / unless you set -health. A release is only promoted when every instance answers it with a status below 500 within deploy.health_timeout.

Configuration

server:
  listen: "0.0.0.0:8080"        # traffic to applications
  admin_listen: "0.0.0.0:8081"  # git, API and dashboard
  base_domain: wharf.local
  state_file: /var/lib/wharf/state.json
  admin_token: "${WHARF_ADMIN_TOKEN}"

git:
  root: /var/lib/wharf/repositories
  branch: main                  # pushes to other branches are ignored
  push_token: "${WHARF_PUSH_TOKEN}"

build:
  work_dir: /var/lib/wharf/builds
  dockerfile: Dockerfile
  timeout: 15m

deploy:
  network: wharf
  health_timeout: 60s
  stop_timeout: 10s
  keep_releases: 5
  log_lines: 500

runtime:
  socket: /var/run/docker.sock

Unknown fields are rejected and every problem is reported at once, so a typo fails at wharf -check rather than at three in the morning. State is one JSON file written through a temporary file and an atomic rename.

Failure behaviour

What goes wrong What Wharf does
The Dockerfile is missing The release fails before anything is built, with that reason recorded
The build fails The release is marked failed; the previous release keeps serving
An instance exits at startup Noticed immediately from its exit code, not after the health timeout
An instance never answers Every new instance is stopped and removed, traffic never moves
A push arrives mid deployment Refused with a clear message rather than run concurrently
Wharf restarts Surviving instances are re-published; releases whose containers are gone are retired
An instance dies later The router reports a bad gateway and the dashboard shows it as unhealthy
A rollback is asked for The previous release is started again and the failing one is stopped

Building and testing

make test     # unit and integration tests
make race     # the whole suite under the race detector
make cover    # coverage summary
make lint     # golangci-lint
make demo     # a working platform on this machine

The suite covers the deployment pipeline against fake build and release layers, so blue and green ordering, rollback, refused concurrent pushes and restart recovery are all asserted without containers. The git server is tested with real git push and git clone against a temporary repository, including token authentication and a path traversal attempt. The container client is tested against recorded runtime responses, and the routing table and reverse proxy against live HTTP servers.

docs/architecture.md covers the release model, the routing switch and the trade offs behind them.

What is next

  • Automatic TLS for published hostnames
  • Deploying to more than one machine, with the router in front of several runtimes
  • Buildpack detection so an application without a Dockerfile still deploys
  • Scheduled jobs and one off commands in the release image
  • Persistent volumes attached to an application

License

MIT. Copyright (c) 2026 Can Sarıhan.

About

Git push deployment platform: builds from your Dockerfile, blue green releases behind health checks, with a router and a live control plane

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages