Setting Up OneDev on Windows Without Docker (Because Docker Wouldn't Start)

This is specifically a Windows story. Wanted to try OneDev — a self-hosted Git server with built-in CI/CD and Kanban boards, lighter than GitLab, more all-in-one than Gitea — on a Windows box. The official install path is docker run ..., which assumes Docker just works. Mine didn't: Docker Desktop wouldn't start at all. If you're on Linux with Docker already working, the official route is simpler and most of this won't apply to you. If you're stuck on Windows without a working Docker Desktop, here's what actually got it running instead — bare-metal, as a plain Java app.

Why Docker didn't work

Docker Desktop's engine returned a 500 — its docker-desktop WSL distro never registered. Installing a plain Ubuntu WSL distro as a workaround also failed:

WSL2 is not supported ... enable the "Virtual Machine Platform" optional component
Error ...CreateVm/HCS/HCS_E_HYPERV_NOT_INSTALLED

Root cause: the Virtual Machine Platform Windows optional feature was off. Both Docker Desktop and WSL2 need it to create their lightweight VM. Hardware virtualization was enabled fine in BIOS (systeminfo reports "A hypervisor has been detected") — this was a pure software/feature-flag issue, not a hardware one. Fixable with a reboot, but I didn't want to reboot the box right then, so:

OneDev is a plain Java application. It sidesteps all of this entirely.

Install (bare-metal, no Docker, no reboot)

# 1. Download the server bundle (~168 MB)
New-Item -ItemType Directory -Force -Path "C:\onedev" | Out-Null
curl.exe -L --fail -o "C:\onedev\onedev-latest.zip" `
  "https://code.onedev.io/onedev/server/~site/onedev-latest.zip"

# 2. Extract
Expand-Archive -Path "C:\onedev\onedev-latest.zip" -DestinationPath "C:\onedev" -Force

# 3. Start in console mode (first boot builds the DB schema, ~30s)
& "C:\onedev\onedev-latest\bin\server.bat" console

When the log prints Please set up the server at http://...:6610, open it in a browser. Prerequisites are just JDK 11+ and Git 2.11.1+ — both were already on this box.

First-run wizard

  1. Create an admin account.
  2. System Setting step — set Server URL to http://localhost:6610 for local-only use (it auto-fills the machine hostname, which only matters if you'll reach OneDev from other devices on the LAN — this URL gets baked into clone URLs, emails, and webhooks). Point Git and curl at "Use in System Path."
  3. Skip SMTP if it's just for local use.

Database is bundled HSQLDB (file-based, zero config) — fine to start, movable to MySQL/Postgres later before anything important lives in it.

Make it a permanent Windows service

Console mode dies when the terminal closes. To auto-start with Windows:

# Elevated prompt — needs admin/UAC, but no reboot
C:\onedev\onedev-latest\bin\server.bat install

Creates a Windows service named OneDev, managed with Start-Service / Stop-Service afterward.

Getting CI working — no Docker means no default executor

OneDev's default job executor is Docker-based, which obviously doesn't work here. Add a Server Shell Executor instead (Administration → Job Executors → Add), so jobs run directly on the host:

  • Job Match must be non-emptyall/true are rejected with "Malformed job match." Use a real query, e.g. "Project" is "myproject".
  • Commands then run in Windows cmd/batch directly, using whatever's on the host (git, PHP, composer, node...). Any container image field in your buildspec is simply ignored.

A working buildspec (schema version 52, OneDev 16.0.1):

version: 52
jobs:
- name: hello-world
  jobExecutor: shell
  steps:
  - !CheckoutStep
    name: checkout
    cloneCredential: !DefaultCredential {}
    condition: SUCCESSFUL
  - !CommandStep
    name: greet
    runInContainer: false
    interpreter: !WindowsBatchInterpreter
      commands: |-
        echo Hello from OneDev CI running on %COMPUTERNAME%
        git --version
    condition: SUCCESSFUL
  triggers:
  - !BranchUpdateTrigger
    userMatch: anyone
  retryCondition: never

Prefer the visual editor ("Add .onedev-buildspec.yml" on the project) — it writes valid YAML for you. Hand-editing means matching the schema exactly, and there are real footguns:

  • `version: 52` is current for 16.0.1 — the number is the highest migrateN method in the server jar, not an incrementing "current version + 1." version: 53 fails with "Cannot find migrate method migrate53."
  • Step condition enum is SUCCESSFUL | ALWAYS | NEVER — not the longer name you'd guess.
  • Interpreter tags are !WindowsBatchInterpreter | !PosixInterpreter | !PowerShellInterpreter. No !DefaultInterpreter for a container-less run.
  • Interpreter commands is a single multi-line string (|- block), not a YAML list.
  • !BranchUpdateTrigger requires userMatch — use anyone, or a real UserCriteria/GroupCriteria. Leaving it empty or writing something like "Name" is "x" both fail validation.
  • Parse/validation errors are silent in the UI. If a build just never appears after a push, check the server log for WARN ... Malformed build spec before assuming something else is wrong.

If you've set up Laravel with Bitbucket Pipelines before, the shape of a build pipeline here will look familiar — same idea (checkout, install, test), different YAML dialect.

Test and coverage reports

OneDev ingests standard report formats (JUnit, Clover, Cobertura, JaCoCo, Jest, Checkstyle, ESLint, PMD, and more) and renders them as build tabs, with coverage diffs on PRs. For PHP:

- !CommandStep
  name: test
  runInContainer: false
  interpreter: !WindowsBatchInterpreter
    commands: |-
      composer install --no-interaction --prefer-dist
      vendor\bin\phpunit --log-junit build\junit.xml --coverage-clover build\clover.xml
  condition: SUCCESSFUL
- !PublishJUnitReportStep
  name: publish tests
  reportName: PHPUnit
  filePatterns: build/junit.xml
  condition: ALWAYS
- !PublishCloverReportStep
  name: publish coverage
  reportName: Coverage
  filePatterns: build/clover.xml
  condition: ALWAYS

You need a coverage driver (Xdebug or PCOV) installed on the PHP the executor uses — without one, PHPUnit silently produces no coverage data at all. And the publish steps must be condition: ALWAYS, not SUCCESSFUL — otherwise a failing test aborts the job before the report ever publishes, and you lose exactly the information you wanted from the failure.

I've got more detail on getting Xdebug/PCOV actually working (including the Mac M1 install quirks) in Adding Code Coverage.

SonarQube — pipeline-level only

There's no native OneDev plugin for SonarQube. It's a plain command step:

- !CommandStep
  name: sonar
  runInContainer: false
  interpreter: !WindowsBatchInterpreter
    commands: |-
      sonar-scanner -Dsonar.host.url=@secret:sonar_url@ -Dsonar.token=@secret:sonar_token@ -Dsonar.qualitygate.wait=true

What you get in OneDev is just the scanner's log output plus a red/green build (red if the quality gate fails). No coverage tab, no findings list, no PR decoration inside OneDev itself — all of that lives in the SonarQube dashboard, since SonarQube doesn't support OneDev as a DevOps platform integration. Store the token as a OneDev job secret rather than hardcoding it. If you don't need a full SonarQube server, OneDev's own Checkstyle/PMD/SpotBugs report steps cover a good chunk of the "quality findings on PRs" use case without standing up Sonar at all.

If you want to go further with SonarQube specifically — including getting free GitHub PR decoration out of the Community edition — I wrote up the gotchas from that separately.

REST API quick reference

Base: http://localhost:6610/~api/. Auth via Authorization: Bearer <token> (create one under your avatar → Access Tokens; it also works as the git-over-HTTP password).

GET /~api/users/me
GET /~api/projects?query=&offset=0&count=N
GET /~api/builds?query="Project" is "myproject"&offset=0&count=N
GET /~api/builds/{id}
GET /~api/settings/job-executors   # POST the full JSON array back to change executors

Source

Subscribe to Building software. Writing what I learn.

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe