Why use Nexus as a Docker Registry?
If you already run Sonatype Nexus and don’t want to stand up Harbor, Nexus can host/pull Docker images and act as a simple Docker proxy/mirror—while also managing Maven/npm/PyPI/etc. Choose Harbor when you need enterprise OCI features (built-in vulnerability scanning, project quotas/RBAC, replication). For a single-box artifact manager covering Docker + other package types, Nexus is lightweight and convenient.
- Prefer Harbor instead? Use my guide: Install Harbor 2.13.1 on Ubuntu 24.04 with HAProxy, Let’s Encrypt (DNS-01), and Systemd
- Need to install Nexus first? Install Sonatype Nexus 3 on Ubuntu 24.04 with HTTPS
TL;DR
- Nexus UI: https://nexus.maksonlee.com→ HAProxy:443 →127.0.0.1:8081
- Docker (hosted): https://nexus.maksonlee.com/v2/...→ HAProxy:443 →127.0.0.1:5001
- Nexus: enable Docker Bearer Token Realm, create Docker (hosted) on HTTP 5001, optional anonymous pulls
- HAProxy: split by path_beg /v2/
Usage:
docker login nexus.maksonlee.com
docker tag ubuntu:22.04 nexus.maksonlee.com/library/ubuntu:22.04
docker push nexus.maksonlee.com/library/ubuntu:22.04
docker pull nexus.maksonlee.com/library/ubuntu:22.04
Assumptions
- Ubuntu 24.04, Nexus 3 OSS, HAProxy 3.2 with a valid cert for nexus.maksonlee.com
- DNS nexus.maksonlee.com→ your HAProxy host
Prerequisites
- Nexus running at 127.0.0.1:8081(UI)
- HAProxy listening on :443 with a full-chain cert
- Enable Docker Realm
Settings → Security → Realms
- Move Docker Bearer Token Realm to Active → Save

- Create a Docker (hosted) Repository
Settings → Repository → Repositories → Create repository → docker (hosted)
- Name: docker-hosted
- HTTP: ✓ Port: 5001
- HTTPS: leave unchecked (TLS handled by HAProxy)
- Enable Docker V1 API: unchecked
- Strict Content Type Validation: checked
- Allow anonymous docker pull: optional
- Blob store: default(or your custom)
- Save

- HAProxy (UI on 8081, Docker on 5001 via /v2/)
Minimal patch from a single-backend setup:
# Redirect HTTP → HTTPS
frontend http_in
    bind *:80
    http-request redirect scheme https code 301 unless { ssl_fc }
# Terminate TLS and route by path
frontend https_in
    bind *:443 ssl crt /etc/haproxy/certs/nexus.maksonlee.com.pem alpn h2,http/1.1
    mode http
    # Docker Registry v2 API is always /v2/
    acl is_docker path_beg -i /v2/
    use_backend nexus_docker if is_docker
    default_backend nexus_ui
backend nexus_ui
    mode http
    option http-buffer-request
    option http-keep-alive
    option forwardfor
    http-request set-header X-Forwarded-Proto https
    http-request set-header Host nexus.maksonlee.com
    server nexus1 127.0.0.1:8081 check
backend nexus_docker
    mode http
    option http-buffer-request
    option http-keep-alive
    option forwardfor
    http-request set-header X-Forwarded-Proto https
    http-request set-header Host nexus.maksonlee.com
    server docker_hosted 127.0.0.1:5001 check
Reload:
sudo systemctl reload haproxy
Result
- https://nexus.maksonlee.com/→ UI (- 127.0.0.1:8081)
- https://nexus.maksonlee.com/v2/...→ Docker hosted (- 127.0.0.1:5001)
- Namespaces (Conventions)
A single-segment name works with this setup (since only one Docker repo is behind /v2/):
docker tag ubuntu:22.04 nexus.maksonlee.com/ubuntu:22.04
docker push nexus.maksonlee.com/ubuntu:22.04
Still, prefer a namespace to stay future-proof (e.g., library/, makson/)—especially if later you enable Path based routing or add more repos behind the same port.
- Minimal RBAC (Privileges & Roles)
Pull-only
- nx-repository-view-docker-docker-hosted-browse
- nx-repository-view-docker-docker-hosted-read
Push (publish)
- nx-repository-view-docker-docker-hosted-browse
- nx-repository-view-docker-docker-hosted-read
- nx-repository-view-docker-docker-hosted-add
- nx-repository-view-docker-docker-hosted-edit
Allow delete (optional)
- nx-repository-view-docker-docker-hosted-delete
All view-level perms (maintainers)
- nx-repository-view-docker-docker-hosted-*
Avoid nx-repository-admin-* unless you want users to change repo settings.
Create roles
- Settings → Security → Roles → Create role
- docker-hosted-pullers→ add browse + read
- docker-hosted-publishers→ add browse + read + add + edit (and optionally delete)
- Settings → Security → Users → assign roles
- (Optional) Anonymous pulls
- Settings → Security → Anonymous Access: enable
- Ensure anonymous effectively has browse + read on docker-hosted(via a read role)
 


- Test From a Client
# Login (TLS via HAProxy on 443)
docker login nexus.maksonlee.com
# Push a test image
docker pull ubuntu:22.04
docker tag ubuntu:22.04 nexus.maksonlee.com/library/ubuntu:22.04
docker push nexus.maksonlee.com/library/ubuntu:22.04
# Pull it back
docker pull nexus.maksonlee.com/library/ubuntu:22.04
Default client port: HTTPS → 443 (no need to specify :443).
Permission Sanity Check (Expected Failures)
Demonstrate auth vs anonymous behavior:
# 1) Push succeeds while logged in (publisher role)
docker login nexus.maksonlee.com
docker push nexus.maksonlee.com/library/ubuntu:22.04
# Expect: success with a digest line
# 2) Logout, push anonymously → should fail
docker logout nexus.maksonlee.com
docker push nexus.maksonlee.com/library/ubuntu:22.04
# Expect:
# ... Layer already exists
# unauthorized: access to the requested resource is not authorized
# 3) Anonymous pull may still work (if enabled)
docker pull nexus.maksonlee.com/library/ubuntu:22.04
# Expect: same digest as pushed
Why it fails: anonymous users never have push rights; push requires the repo view-level add/edit privileges.
