Local docker-compose stacks that build and run GitLab from source for development — one for CE (FOSS) and one for EE. Each stack builds the Rails app + Workhorse from a local source checkout and runs Postgres, Redis, and Gitaly (prebuilt images) alongside. The headline feature is a hot-reload frontend mode (webpack dev server behind an nginx proxy) so you can edit JS/Vue and see the change on refresh — plus faster unminified asset builds.
The two stacks are independent (separate compose projects, volumes, and host ports) and can run at the same time.
gitlab-ee-dev/ # this repo (tooling only — no GitLab source)
├── ce/ # CE (FOSS) stack
│ ├── docker-compose.dev.yml
│ ├── dev.sh Makefile # wrappers (same targets)
│ ├── Dockerfile # app image (Rails + Workhorse)
│ ├── Dockerfile.assets-build # assets image (deps + compile stages)
│ ├── build-assets-fast.sh # host-native full asset build (populates public/assets)
│ ├── .env env.example
│ └── docker/docker-entrypoint.sh docker/nginx-dev-proxy.conf docker/gitaly-config.toml
└── ee/ # EE stack (same shape, EE naming/ports)
../gitlab-foss # CE source tree (ce/ default GITLAB_SRC)
../gitlab-v18.11.4-ee # EE source tree (ee/ default GITLAB_SRC)
Gitaly always runs from the official CNG prebuilt image, so no Gitaly source checkout is needed.
- Docker with Buildx (Compose v2).
- The GitLab source checkout for your edition (path configurable — see
GITLAB_SRC). - For
hotreloadmode (andbuild-assets-fast.sh), a one-time host toolchain for compiling assets natively — see the header ofce/build-assets-fast.shfor theapt-getlist. The Docker asset builds install their own build deps inside the image. - Assets/webpack builds are memory-hungry (~6–8 GB). Free RAM before a cold build.
cd ce # or: cd ee
cp env.example .env # then set GITLAB_SRC + secrets (GITLAB_HOST defaults to localhost)
./dev.sh up # or: make upThen browse:
- CE → http://localhost:8182
- EE → http://localhost:8181
Log in with GITLAB_ROOT_EMAIL / GITLAB_ROOT_PASSWORD from .env.
| Mode | What runs | Use for |
|---|---|---|
prebuilt |
app built FROM the official registry assets image; nothing compiled locally | fastest bring-up; no frontend work |
full |
local assets image, production/minified | shippable-like static assets |
fast |
local assets image, unminified (skips terser, webpack's ~90% cost) | quick static assets for UI testing |
hotreload |
webpack dev server + nginx proxy (compose hotreload profile); Rails fetches the manifest live |
live iteration — edit JS/Vue → browser auto-reloads |
ASSETS_MODE also selects how the app image sources assets (it derives the ASSETS_SOURCE
build-arg internally), so it's the only assets knob you set.
With ASSETS_MODE=hotreload, ./dev.sh up starts two extra services:
assets-watch— runswebpack-dev-serveragainst the mounted source (in-memory bundles + HMR).proxy— nginx on the host port, routing/assets/webpack/*+/wsto the dev server and everything else to Workhorse (so the browser stays same-origin).
Edit any .js/.vue under <source>/app/assets/javascripts/ → webpack rebuilds in-container →
the page auto-reloads. (GitLab wires live-reload — a full page refresh — rather than
state-preserving HMR for most components.)
Prerequisite: the source tree's public/assets must already hold CSS/images/sprockets
output (the dev server only rebuilds the JS). Populate it once with:
./build-assets-fast.sh # full unminified compile on the host, writes <source>/public/assetsAdding a brand-new chunk (new entry point / dynamic import) changes the manifest; restart
web to pick it up: docker compose -f docker-compose.dev.yml restart web.
| Var | Meaning |
|---|---|
GITLAB_HOST / GITLAB_PORT / GITLAB_HTTPS |
external URL the browser uses (CE 8182, EE 8181). GITLAB_HOST is optional — defaults to localhost; don't use 0.0.0.0 (it's a bind address, not a reachable host). |
GITLAB_SRC |
GitLab source tree (absolute, or relative to the stack dir) — set this |
GITLAB_VERSION |
bare version tag (e.g. 18.11.4); image/registry tags add a leading v |
ASSETS_MODE |
prebuilt | full | fast | hotreload (see above) |
WORKHORSE_SOURCE |
source (compile) or prebuilt (official CNG image). Gitaly is always prebuilt. |
POSTGRES_*, GITALY_AUTH_TOKEN, GITLAB_SHELL_SECRET, GITLAB_WORKHORSE_SECRET |
infra + shared secrets |
GITLAB_ROOT_EMAIL / GITLAB_ROOT_PASSWORD |
initial admin, seeded on first boot |
To target another release: set GITLAB_VERSION and point GITLAB_SRC at a matching checkout,
then rebuild.
| Command | Action |
|---|---|
up |
build as needed, then start the stack (applies ASSETS_MODE) |
build |
build the images only |
assets |
build the local assets image if missing |
assets-rebuild |
force-rebuild the local assets image (honors ASSETS_MODE) |
restart |
recreate web/workhorse/sidekiq |
stop / down |
stop / remove containers (volumes kept) |
logs |
tail web logs |
ps |
stack status |
Watch webpack rebuilds in hotreload mode:
docker compose -f docker-compose.dev.yml logs -f assets-watch
| CE | EE | |
|---|---|---|
| Browser / proxy | 8182 | 8181 |
| Workhorse (hotreload alt) | 8183 | 8184 |
| Dev server (internal only) | 3808 | 3808 |
- The GitLab source trees are not modified. The stacks bind-mount source read-only where
needed; the webpack-dev-server overlay tweak is applied via a CLI flag
(
--no-client-overlay-warnings), not by editingconfig/webpack.config.js. Generated config (config/gitlab.yml, etc.) is rendered bydocker/docker-entrypoint.shat container boot. - CSP is disabled in hotreload mode (dev-only) so the browser can load bundles same-origin.
- First hotreload boot: give
assets-watch~1 min for its initial compile (assets 502 until done), then hard-refresh once so the browser picks up the current HMR client. - Switching modes with
./dev.sh up/make upcleans up the hotreload-only services so the host port is handed back to Workhorse.
See CLAUDE.md for an internals-oriented map of how the pieces fit together.