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.
$ 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 completeThe 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.
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
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.
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 dashboardOr 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-downApplications 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.
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.
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| 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.
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.
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.sockUnknown 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.
| 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 |
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 machineThe 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.
- 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
MIT. Copyright (c) 2026 Can Sarıhan.
