Nexus Repository Manager: Proxy Setup for npm, PyPI, Terraform, NuGet, Maven, and Docker
Quick answer
Run Nexus Repository Manager as a caching proxy for every package ecosystem your team uses — npm, PyPI, Terraform, NuGet, Maven, and Docker. Covers installation, blob stores, repo groups, per-ecosystem client config, and CI/CD integration.
intermediate · 90 min
Before you begin
- Linux server or VM with 4 GB RAM minimum (8 GB recommended)
- Docker and Docker Compose installed, or Java 21 for bare-metal install (Nexus requires Java 21)
- Familiarity with package managers for the ecosystems you'll proxy
Nexus Repository Manager: Proxy Setup for npm, PyPI, Terraform, NuGet, Maven, and Docker
A proxy repository sits between your developers (and CI runners) and the public package registries. Nexus downloads the artifact on first request, caches it locally, and serves it from cache on every subsequent request. The wins are immediate: faster builds (no round-trip to the internet), guaranteed availability when the upstream is down, and a single auditable choke point for everything your team installs.
This tutorial installs Nexus OSS, explains the core repository model, then walks through proxy and client configuration for every major ecosystem.
Why a Proxy Repository
- Speed — cached artifacts serve at LAN speed rather than over the internet
- Reliability — builds don't break when npmjs.org, PyPI, or Docker Hub has an outage
- Air-gapped builds — once the cache is warm, CI runners need no outbound internet access
- Audit and compliance — every package request goes through one place; you can enforce allow-lists and scan for vulnerabilities
- Bandwidth — one download for 100 developers instead of 100 downloads
Install Nexus with Docker Compose
The easiest production-ready setup. Nexus needs at least 4 GB of RAM; the JVM defaults are conservative — size up with INSTALL4J_ADD_VM_PARAMS.
1# docker-compose.yml
2services:
3 nexus:
4 image: sonatype/nexus3:latest
5 container_name: nexus
6 ports:
7 - "8081:8081" # Nexus UI and all non-Docker repos
8 - "8082:8082" # Docker proxy registry (separate port — Docker protocol requires it)
9 - "8083:8083" # Docker hosted registry (optional)
10 volumes:
11 - nexus-data:/nexus-data
12 environment:
13 INSTALL4J_ADD_VM_PARAMS: "-Xms2g -Xmx2g -XX:MaxDirectMemorySize=3g"
14 restart: unless-stopped
15
16volumes:
17 nexus-data:docker compose up -d
# Tail logs until you see "Started Sonatype Nexus"
docker compose logs -f nexusFirst login: navigate to http://<server>:8081. The UI prompts for a password — retrieve it from the container:
docker compose exec nexus cat /nexus-data/admin.passwordLog in as admin, set a new password, and disable anonymous access unless you have a specific reason to allow it.
Bare-metal install (systemd)
1# Download the latest OSS tarball from https://help.sonatype.com/en/download.html
2tar -xzf nexus-3.*.tar.gz -C /opt
3ln -s /opt/nexus-3.* /opt/nexus
4
5# Create a dedicated service user
6useradd -r -d /opt/sonatype-work -s /sbin/nologin nexus
7chown -R nexus:nexus /opt/nexus /opt/sonatype-work
8
9# Set the run-as user
10echo "run_as_user=\"nexus\"" > /opt/nexus/bin/nexus.rc1# /etc/systemd/system/nexus.service
2[Unit]
3Description=Sonatype Nexus Repository Manager
4After=network.target
5
6[Service]
7Type=forking
8User=nexus
9ExecStart=/opt/nexus/bin/nexus start
10ExecStop=/opt/nexus/bin/nexus stop
11Restart=on-failure
12
13[Install]
14WantedBy=multi-user.targetsystemctl daemon-reload
systemctl enable --now nexusCore Concepts
Repository types
| Type | Purpose |
|---|---|
| proxy | Caches an upstream registry — this is what most of this tutorial is about |
| hosted | Stores internally published artifacts (your own packages) |
| group | Virtual repo that combines proxy + hosted behind one URL — what clients point to |
The recommended pattern for every ecosystem is:
<format>-proxy → upstream public registry
<format>-hosted → your internal artifacts
<format>-group → proxy + hosted combined (clients use this URL)
Blob stores
Blob stores are where Nexus physically writes artifact data. The default blob store (default) stores everything in /nexus-data/blobs/. For production, create one blob store per ecosystem — it makes quota management and cleanup policies straightforward.
Settings → Blob Stores → Create Blob Store
- Type: File
- Name:
npm-blobs,pypi-blobs, etc. - Path: accepts the default under
/nexus-data/blobs/
For cloud deployments, use an S3 blob store (Nexus Pro) or mount an S3-backed volume.
Security Realms
Nexus uses Security Realms to handle format-specific authentication flows (bearer tokens, API keys). Without the right realm active, npm login, docker login, and NuGet API key auth silently fail even if the repository exists and the credentials are correct.
Security → Realms
Drag these from the Available column to Active, then Save:
| Realm | Required for |
|---|---|
| npm Bearer Token Realm | npm login / token-based publish |
| Docker Bearer Token Realm | docker login against Nexus |
| NuGet API-Key Realm | NuGet API key authentication |
| PyPI Bearer Token Realm | pip / twine token authentication |
The Local Authenticating and Local Authorizing realms are active by default and must stay at the top of the list. Add the format realms below them.
HTTP Basic auth (username + password) works for most ecosystems without a realm, but bearer token flows — which all modern CLI tools prefer — require the matching realm to be active.
Creating a proxy repo (UI)
Every proxy repo in this tutorial follows the same creation flow:
- Repositories → Create repository
- Select the format + type (e.g.,
npm (proxy)) - Set a name (e.g.,
npm-proxy) - Paste the upstream URL
- Select the blob store
- Save
The REST API equivalent (useful for automation):
1curl -u admin:PASSWORD -X POST \
2 http://nexus:8081/service/rest/v1/repositories/<format>/proxy \
3 -H "Content-Type: application/json" \
4 -d '{
5 "name": "<format>-proxy",
6 "online": true,
7 "storage": { "blobStoreName": "<format>-blobs", "strictContentTypeValidation": true },
8 "proxy": { "remoteUrl": "<upstream-url>", "contentMaxAge": 1440, "metadataMaxAge": 1440 },
9 "negativeCache": { "enabled": true, "timeToLive": 1440 },
10 "httpClient": { "blocked": false, "autoBlock": true }
11 }'npm
Create the proxy repo
| Field | Value |
|---|---|
| Format + type | npm (proxy) |
| Name | npm-proxy |
| Remote URL | https://registry.npmjs.org |
| Blob store | npm-blobs |
Create a group repo (npm-group) that includes npm-hosted and npm-proxy. Clients point to the group.
Client config
# Per-project — create .npmrc in the repo root
echo "registry=http://nexus:8081/repository/npm-group/" > .npmrc
# Or set globally
npm config set registry http://nexus:8081/repository/npm-group/Realm required. Token-based npm auth (
npm login) only works after enabling the npm Bearer Token Realm under Security → Realms.
If Nexus requires authentication (recommended — disable anonymous access):
1# Generate a base64-encoded user:password token
2echo -n "admin:PASSWORD" | base64
3# → YWRtaW46UEFTU1dPUkQ=
4
5cat >> .npmrc <<'EOF'
6//nexus:8081/repository/npm-group/:_auth=YWRtaW46UEFTU1dPUkQ=
7//nexus:8081/repository/npm-group/:always-auth=true
8EOFFor publishing internal packages to npm-hosted:
# .npmrc
registry=http://nexus:8081/repository/npm-group/
//nexus:8081/repository/npm-hosted/:_auth=YWRtaW46UEFTU1dPUkQ=npm publish --registry http://nexus:8081/repository/npm-hosted/Verify
npm install --prefer-online lodash # first request — proxied from npmjs.org
npm install lodash # second request — served from Nexus cachePyPI
Create the proxy repo
| Field | Value |
|---|---|
| Format + type | pypi (proxy) |
| Name | pypi-proxy |
| Remote URL | https://pypi.org |
| Blob store | pypi-blobs |
Client config
pip:
1# /etc/pip.conf or /etc/xdg/pip/pip.conf (system-wide)
2# ~/.config/pip/pip.conf (user — modern preferred path)
3# ~/.pip/pip.conf (user — legacy, still works)
4# .pip/pip.conf (project)
5[global]
6index-url = http://nexus:8081/repository/pypi-proxy/simple/
7trusted-host = nexusThe /simple/ suffix is required — it's the PEP 503 simple index path that pip uses.
With credentials:
[global]
index-url = http://admin:PASSWORD@nexus:8081/repository/pypi-proxy/simple/Or use environment variables (preferred in CI):
export PIP_INDEX_URL=http://admin:PASSWORD@nexus:8081/repository/pypi-proxy/simple/Poetry:
# pyproject.toml
[[tool.poetry.source]]
name = "nexus"
url = "http://nexus:8081/repository/pypi-proxy/simple/"
priority = "primary"poetry config http-basic.nexus admin PASSWORDuv:
uv pip install requests \
--index-url http://nexus:8081/repository/pypi-proxy/simple/ \
--trusted-host nexus
# Or via environment variable
export UV_INDEX_URL=http://nexus:8081/repository/pypi-proxy/simple/Verify
pip install requests # downloads from nexus, caches internally
pip install requests # served from Nexus cache — noticeably fasterTerraform / OpenTofu Registry
Nexus 3.88.0+ supports the Terraform registry protocol. It proxies the public provider and module registries.
Create the proxy repo
| Field | Value |
|---|---|
| Format + type | terraform (proxy) |
| Name | terraform-proxy |
| Remote URL | https://registry.terraform.io |
| Blob store | terraform-blobs |
Client config
Create or update ~/.terraformrc (Linux/macOS) or %APPDATA%/terraform.rc (Windows):
1# ~/.terraformrc
2provider_installation {
3 network_mirror {
4 url = "https://nexus.internal/repository/terraform-proxy/"
5 include = ["registry.terraform.io/*/*"]
6 }
7 direct {
8 exclude = ["registry.terraform.io/*/*"]
9 }
10}The trailing slash in the URL is required. include patterns follow the <hostname>/<namespace>/<type> format.
HTTPS required. HashiCorp's docs state network mirror URLs must use the
https:scheme — Terraform verifies the TLS certificate to establish mirror identity. Put Nexus behind a reverse proxy with a valid certificate (nginx + Let's Encrypt, or your internal CA) before using it as a Terraform mirror. HTTP-only Nexus instances are not usable fornetwork_mirror; use a hosted repository with directsourceoverrides in that case.
For authentication, Nexus uses HTTP Basic auth. Set credentials via an environment variable (avoid hardcoding in .terraformrc):
1export TF_CLI_ARGS_init="-input=false"
2# Credentials via .netrc (Terraform reads ~/.netrc)
3cat >> ~/.netrc <<EOF
4machine nexus
5 login admin
6 password PASSWORD
7EOF
8chmod 600 ~/.netrcPer-project override
For a single project without touching the global ~/.terraformrc:
1# terraform.tf (or any .tf file in the project)
2terraform {
3 required_providers {
4 aws = {
5 source = "hashicorp/aws"
6 version = "~> 5.0"
7 }
8 }
9}Terraform resolves the provider source through ~/.terraformrc — no per-project config is needed beyond the global mirror declaration.
OpenTofu
OpenTofu reads ~/.tofurc instead of ~/.terraformrc. Same format, same URL — just rename the file:
cp ~/.terraformrc ~/.tofurcVerify
terraform init # providers download through Nexus and are cachedNuGet
Create the proxy repo
| Field | Value |
|---|---|
| Format + type | nuget (proxy) |
| Name | nuget-proxy |
| Remote URL | https://api.nuget.org/v3/index.json |
| Blob store | nuget-blobs |
Client config
Realm required for API key auth. If you use NuGet API keys instead of username/password, enable the NuGet API-Key Realm under Security → Realms. Basic auth (as shown below) works without it.
nuget.config (project or user-level):
1<?xml version="1.0" encoding="utf-8"?>
2<configuration>
3 <packageSources>
4 <clear />
5 <add key="nexus" value="http://nexus:8081/repository/nuget-proxy/index.json" />
6 </packageSources>
7 <packageSourceCredentials>
8 <nexus>
9 <add key="Username" value="admin" />
10 <add key="ClearTextPassword" value="PASSWORD" />
11 </nexus>
12 </packageSourceCredentials>
13</configuration>Place this in the project root (committed, credentials removed) or at %APPDATA%\NuGet\NuGet.Config / ~/.nuget/NuGet/NuGet.Config (user-level, with credentials).
dotnet CLI:
dotnet nuget add source \
http://nexus:8081/repository/nuget-proxy/index.json \
--name nexus \
--username admin \
--password PASSWORD \
--store-password-in-clear-textCI (environment variables):
dotnet nuget add source \
http://nexus:8081/repository/nuget-proxy/index.json \
--name nexus \
--username "$NEXUS_USER" \
--password "$NEXUS_PASS" \
--store-password-in-clear-textVerify
dotnet add package Newtonsoft.Json # first: proxied from nuget.org; second: cachedMaven and Gradle
Create the proxy repo
| Field | Value |
|---|---|
| Format + type | maven2 (proxy) |
| Name | maven-central-proxy |
| Remote URL | https://repo1.maven.org/maven2/ |
| Blob store | maven-blobs |
| Version policy | Release |
Create a second proxy for snapshots if needed:
- Name:
maven-snapshots-proxy - Remote URL:
https://central.sonatype.com/repository/maven-snapshots/(the legacy OSSRH hostoss.sonatype.orgwas retired in June 2025) - Version policy: Snapshot
Create a group (maven-group) that combines them.
Maven (~/.m2/settings.xml)
1<settings>
2 <mirrors>
3 <mirror>
4 <id>nexus</id>
5 <mirrorOf>*</mirrorOf>
6 <url>http://nexus:8081/repository/maven-group/</url>
7 </mirror>
8 </mirrors>
9 <servers>
10 <server>
11 <id>nexus</id>
12 <username>admin</username>
13 <password>PASSWORD</password>
14 </server>
15 </servers>
16</settings><mirrorOf>*</mirrorOf> redirects all repository requests through Nexus.
Gradle (build.gradle or settings.gradle)
1// settings.gradle (Gradle 7+ — apply to all projects)
2dependencyResolutionManagement {
3 repositories {
4 maven {
5 url "http://nexus:8081/repository/maven-group/"
6 credentials {
7 username = System.getenv("NEXUS_USER") ?: "admin"
8 password = System.getenv("NEXUS_PASS") ?: "PASSWORD"
9 }
10 }
11 }
12}For Kotlin DSL (settings.gradle.kts):
1dependencyResolutionManagement {
2 repositories {
3 maven {
4 url = uri("http://nexus:8081/repository/maven-group/")
5 credentials {
6 username = System.getenv("NEXUS_USER") ?: "admin"
7 password = System.getenv("NEXUS_PASS") ?: "PASSWORD"
8 }
9 }
10 }
11}Docker
Nexus supports two Docker routing modes. Path-based routing (the current recommended approach) encodes the repository name in the image namespace and requires no separate port — everything goes through port 8081. Port-based routing assigns each Docker repository its own TCP port (legacy approach). This tutorial uses port-based routing for Docker Hub because it allows registry-mirrors in daemon.json, which automatically redirects all docker pull calls without changing image references. The Docker Compose setup above already exposes port 8082 for this.
Create the proxy repo
Repositories → Create repository → docker (proxy)
| Field | Value |
|---|---|
| Name | docker-proxy |
| HTTP port | 8082 |
| Allow anonymous docker pull | Enable if you disable anonymous Nexus access globally but still want unauthenticated pulls |
| Remote storage URL | https://registry-1.docker.io |
| Docker index | Use Docker Hub |
| Blob store | docker-blobs |
For other registries, create separate proxy repos:
| Registry | Remote storage URL |
|---|---|
| Docker Hub | https://registry-1.docker.io (index: Use Docker Hub) |
| GCR | https://gcr.io |
| Quay | https://quay.io |
| GitHub Container Registry | https://ghcr.io |
| ECR Public | https://public.ecr.aws |
Client config
Realm required.
docker loginagainst Nexus requires the Docker Bearer Token Realm to be active under Security → Realms. Without it, login returns 401 even with correct credentials.
For an HTTP (non-TLS) Nexus — add to Docker daemon config:
// /etc/docker/daemon.json
{
"insecure-registries": ["nexus:8082"]
}systemctl restart dockerMirror Docker Hub pulls through Nexus (all docker pull image:tag calls go through Nexus automatically):
// /etc/docker/daemon.json
{
"registry-mirrors": ["http://nexus:8082"],
"insecure-registries": ["nexus:8082"]
}With registry-mirrors, docker pull nginx:alpine automatically routes through nexus:8082 without any image tag changes.
For other registries, prefix the image with the Nexus host:
# Pull gcr.io image through Nexus GCR proxy (port 8084 if you created one)
docker pull nexus:8084/google-containers/pause:3.9
# Or for authenticated pulls
docker login nexus:8082 -u admin -p PASSWORD
docker pull nexus:8082/library/alpine:3.19In Kubernetes — configure containerd to use Nexus as a mirror via hosts.toml (the current approach; the old inline registry.mirrors in config.toml is deprecated since containerd 1.4):
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"1# Docker Hub mirror
2mkdir -p /etc/containerd/certs.d/docker.io
3cat > /etc/containerd/certs.d/docker.io/hosts.toml <<'EOF'
4server = "https://registry-1.docker.io"
5
6[host."http://nexus:8082"]
7 capabilities = ["pull", "resolve"]
8EOF
9
10# GCR mirror (if you created a separate proxy on port 8084)
11mkdir -p /etc/containerd/certs.d/gcr.io
12cat > /etc/containerd/certs.d/gcr.io/hosts.toml <<'EOF'
13server = "https://gcr.io"
14
15[host."http://nexus:8084"]
16 capabilities = ["pull", "resolve"]
17EOFsystemctl restart containerdCI/CD Integration
Store credentials in CI secrets, never in committed files.
GitHub Actions
1# .github/workflows/build.yml
2env:
3 NEXUS_URL: http://nexus:8081
4 NEXUS_USER: ${{ secrets.NEXUS_USER }}
5 NEXUS_PASS: ${{ secrets.NEXUS_PASS }}
6
7jobs:
8 build:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v4
12
13 # npm
14 - name: Configure npm registry
15 run: |
16 echo "registry=${{ env.NEXUS_URL }}/repository/npm-group/" >> .npmrc
17 echo "//${{ env.NEXUS_URL | replace('http://', '') }}/repository/npm-group/:_auth=$(echo -n $NEXUS_USER:$NEXUS_PASS | base64)" >> .npmrc
18
19 # pip
20 - name: Configure pip
21 run: |
22 pip config set global.index-url \
23 http://${{ env.NEXUS_USER }}:${{ env.NEXUS_PASS }}@nexus:8081/repository/pypi-proxy/simple/
24
25 # Docker
26 - name: Log in to Nexus Docker registry
27 run: echo "$NEXUS_PASS" | docker login nexus:8082 -u "$NEXUS_USER" --password-stdin
28
29 # Maven
30 - name: Configure Maven settings
31 run: |
32 mkdir -p ~/.m2
33 cat > ~/.m2/settings.xml <<EOF
34 <settings>
35 <mirrors>
36 <mirror>
37 <id>nexus</id>
38 <mirrorOf>*</mirrorOf>
39 <url>${{ env.NEXUS_URL }}/repository/maven-group/</url>
40 </mirror>
41 </mirrors>
42 <servers>
43 <server>
44 <id>nexus</id>
45 <username>${{ env.NEXUS_USER }}</username>
46 <password>${{ env.NEXUS_PASS }}</password>
47 </server>
48 </servers>
49 </settings>
50 EOFGitLab CI
1# .gitlab-ci.yml
2variables:
3 PIP_INDEX_URL: "http://$NEXUS_USER:$NEXUS_PASS@nexus:8081/repository/pypi-proxy/simple/"
4 NPM_CONFIG_REGISTRY: "http://nexus:8081/repository/npm-group/"
5
6before_script:
7 - echo "//${CI_SERVER_HOST}/repository/npm-group/:_auth=$(echo -n $NEXUS_USER:$NEXUS_PASS | base64)" >> .npmrc
8 - echo "http://nexus:8081/repository/npm-group/:always-auth=true" >> .npmrcMaintenance
Cleanup policies
Left unchecked, the blob store grows indefinitely. Create a cleanup policy under Administration → Cleanup Policies:
- Last downloaded: remove artifacts not accessed in 90 days
- Released before: remove old release versions after N days
Attach the policy to each repository, then run the cleanup task manually or schedule it.
Storage quota
Administration → Blob Stores → edit a blob store → set a soft quota. Nexus triggers a warning (or blocks writes) when the quota is exceeded.
Scheduled tasks
Administration → Tasks → Create task:
Cleanup service— runs the attached cleanup policiesCompact blob store— reclaims disk space after cleanup (runs after cleanup)Rebuild repository index— if search becomes inconsistent
Frequently Asked Questions
Why run a proxy repository at all?
Resilience and speed, mainly. Builds keep working when an upstream registry is unavailable or removes a package, artifacts are cached close to your runners, and you get a single place to see what your organisation actually depends on. It also gives you somewhere to block a package if you have to.
Does a proxy protect against a compromised upstream package?
Not by itself — it caches whatever upstream served. What it gives you is a chokepoint where scanning and blocking can be applied, and a cache that keeps a known-good version available after upstream changes. Pair it with a scanner and lockfiles.
What is the difference between proxy, hosted and group repositories?
A proxy caches an upstream. A hosted repository stores your own artifacts. A group presents several as one endpoint, so clients configure a single URL and Nexus resolves across the members. Point clients at the group rather than the individual repositories.
How much storage should I plan for?
More than you expect, and set cleanup policies from the start. Proxy caches grow steadily and language ecosystems with many versions grow quickly. Configure retention early — reclaiming space on a full disk under pressure is far less pleasant than never filling it.
What's Next
- GitHub Actions: CI/CD for Containers and Kubernetes — automate the pipelines that consume these proxy repos
- Helm Fundamentals: Kubernetes Package Manager — Nexus also proxies Helm chart repositories via the
helm (proxy)format - Supply Chain Security: Sigstore and SLSA — sign and verify the artifacts flowing through your Nexus instance
Official References
- Terraform documentation — configuration language, state and provider behaviour
- Terraform state — remote backends, locking and drift
- Dockerfile best practices — layer caching, image size and build ordering
- Dockerfile reference — every instruction and its semantics
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.