DevOps & Platform

Nexus Repository Manager: Proxy Setup for npm, PyPI, Terraform, NuGet, Maven, and Docker

Intermediate90 min to complete25 min readJune 3, 2026Updated August 19, 2026

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
Artifact Management
npm
PyPI
Terraform
Docker
DevOps
Platform Engineering

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.

yaml
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:
bash
docker compose up -d

# Tail logs until you see "Started Sonatype Nexus"
docker compose logs -f nexus

First login: navigate to http://<server>:8081. The UI prompts for a password — retrieve it from the container:

bash
docker compose exec nexus cat /nexus-data/admin.password

Log in as admin, set a new password, and disable anonymous access unless you have a specific reason to allow it.

Bare-metal install (systemd)

bash
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.rc
ini
1# /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.target
bash
systemctl daemon-reload
systemctl enable --now nexus

Core Concepts

Repository types

TypePurpose
proxyCaches an upstream registry — this is what most of this tutorial is about
hostedStores internally published artifacts (your own packages)
groupVirtual 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:

RealmRequired for
npm Bearer Token Realmnpm login / token-based publish
Docker Bearer Token Realmdocker login against Nexus
NuGet API-Key RealmNuGet API key authentication
PyPI Bearer Token Realmpip / 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:

  1. Repositories → Create repository
  2. Select the format + type (e.g., npm (proxy))
  3. Set a name (e.g., npm-proxy)
  4. Paste the upstream URL
  5. Select the blob store
  6. Save

The REST API equivalent (useful for automation):

bash
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

FieldValue
Format + typenpm (proxy)
Namenpm-proxy
Remote URLhttps://registry.npmjs.org
Blob storenpm-blobs

Create a group repo (npm-group) that includes npm-hosted and npm-proxy. Clients point to the group.

Client config

bash
# 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):

bash
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
8EOF

For publishing internal packages to npm-hosted:

bash
# .npmrc
registry=http://nexus:8081/repository/npm-group/
//nexus:8081/repository/npm-hosted/:_auth=YWRtaW46UEFTU1dPUkQ=
bash
npm publish --registry http://nexus:8081/repository/npm-hosted/

Verify

bash
npm install --prefer-online lodash   # first request — proxied from npmjs.org
npm install lodash                   # second request — served from Nexus cache

PyPI

Create the proxy repo

FieldValue
Format + typepypi (proxy)
Namepypi-proxy
Remote URLhttps://pypi.org
Blob storepypi-blobs

Client config

pip:

ini
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 = nexus

The /simple/ suffix is required — it's the PEP 503 simple index path that pip uses.

With credentials:

ini
[global]
index-url = http://admin:PASSWORD@nexus:8081/repository/pypi-proxy/simple/

Or use environment variables (preferred in CI):

bash
export PIP_INDEX_URL=http://admin:PASSWORD@nexus:8081/repository/pypi-proxy/simple/

Poetry:

toml
# pyproject.toml
[[tool.poetry.source]]
name = "nexus"
url = "http://nexus:8081/repository/pypi-proxy/simple/"
priority = "primary"
bash
poetry config http-basic.nexus admin PASSWORD

uv:

bash
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

bash
pip install requests   # downloads from nexus, caches internally
pip install requests   # served from Nexus cache — noticeably faster

Terraform / OpenTofu Registry

Nexus 3.88.0+ supports the Terraform registry protocol. It proxies the public provider and module registries.

Create the proxy repo

FieldValue
Format + typeterraform (proxy)
Nameterraform-proxy
Remote URLhttps://registry.terraform.io
Blob storeterraform-blobs

Client config

Create or update ~/.terraformrc (Linux/macOS) or %APPDATA%/terraform.rc (Windows):

hcl
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 for network_mirror; use a hosted repository with direct source overrides in that case.

For authentication, Nexus uses HTTP Basic auth. Set credentials via an environment variable (avoid hardcoding in .terraformrc):

bash
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 ~/.netrc

Per-project override

For a single project without touching the global ~/.terraformrc:

hcl
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:

bash
cp ~/.terraformrc ~/.tofurc

Verify

bash
terraform init   # providers download through Nexus and are cached

NuGet

Create the proxy repo

FieldValue
Format + typenuget (proxy)
Namenuget-proxy
Remote URLhttps://api.nuget.org/v3/index.json
Blob storenuget-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):

xml
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:

bash
dotnet nuget add source \
  http://nexus:8081/repository/nuget-proxy/index.json \
  --name nexus \
  --username admin \
  --password PASSWORD \
  --store-password-in-clear-text

CI (environment variables):

bash
dotnet nuget add source \
  http://nexus:8081/repository/nuget-proxy/index.json \
  --name nexus \
  --username "$NEXUS_USER" \
  --password "$NEXUS_PASS" \
  --store-password-in-clear-text

Verify

bash
dotnet add package Newtonsoft.Json   # first: proxied from nuget.org; second: cached

Maven and Gradle

Create the proxy repo

FieldValue
Format + typemaven2 (proxy)
Namemaven-central-proxy
Remote URLhttps://repo1.maven.org/maven2/
Blob storemaven-blobs
Version policyRelease

Create a second proxy for snapshots if needed:

  • Name: maven-snapshots-proxy
  • Remote URL: https://central.sonatype.com/repository/maven-snapshots/ (the legacy OSSRH host oss.sonatype.org was retired in June 2025)
  • Version policy: Snapshot

Create a group (maven-group) that combines them.

Maven (~/.m2/settings.xml)

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)

groovy
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):

kotlin
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)

FieldValue
Namedocker-proxy
HTTP port8082
Allow anonymous docker pullEnable if you disable anonymous Nexus access globally but still want unauthenticated pulls
Remote storage URLhttps://registry-1.docker.io
Docker indexUse Docker Hub
Blob storedocker-blobs

For other registries, create separate proxy repos:

RegistryRemote storage URL
Docker Hubhttps://registry-1.docker.io (index: Use Docker Hub)
GCRhttps://gcr.io
Quayhttps://quay.io
GitHub Container Registryhttps://ghcr.io
ECR Publichttps://public.ecr.aws

Client config

Realm required. docker login against 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:

json
// /etc/docker/daemon.json
{
  "insecure-registries": ["nexus:8082"]
}
bash
systemctl restart docker

Mirror Docker Hub pulls through Nexus (all docker pull image:tag calls go through Nexus automatically):

json
// /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:

bash
# 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.19

In 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):

toml
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry]
  config_path = "/etc/containerd/certs.d"
bash
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"]
17EOF
bash
systemctl restart containerd

CI/CD Integration

Store credentials in CI secrets, never in committed files.

GitHub Actions

yaml
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          EOF

GitLab CI

yaml
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" >> .npmrc

Maintenance

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 policies
  • Compact 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

Official References

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.