After successfully deploying a Java application to DigitalOcean (see previous post), I moved on to the next challenge: setting up a centralized artifact repository. In that first project, I manually copied JAR files from my laptop to the server using scp. That works for learning, but it doesn't scale. In a real development team, you need a central place to store build artifacts—a "source of truth" where everyone pulls from the same versioned releases.

Enter Nexus Repository Manager.

Nexus is an artifact repository that stores compiled software artifacts—JAR files, Docker images, npm packages, Python wheels, whatever you're building. It's like GitHub, but for compiled code instead of source code. When your CI/CD pipeline builds an application, it publishes the artifact to Nexus. When you deploy that application, you pull the exact same artifact from Nexus.

This seemed straightforward. Install Nexus, configure a repository, publish some JARs. Maybe an hour or two.

It took me an entire afternoon (about 4 hours). And I learned more about CPU architectures, Linux process management, and permission systems than I bargained for.

The Plan (And What Actually Happened)

What I thought I'd do:

  1. Install Nexus on a DigitalOcean droplet (15 minutes)
  2. Configure a Maven repository (15 minutes)
  3. Publish artifacts from Gradle and Maven projects (30 minutes)

What actually happened:

  1. Nexus wouldn't start (silent failure, no logs)
  2. Discovered I'd downloaded the wrong CPU architecture
  3. Rebuilt the entire server with the correct version
  4. Spent hours understanding Nexus's RBAC permission model
  5. Configured Gradle and Maven publishing workflows from scratch
  6. Learned about blob stores and cleanup policies the hard way

But let's start from the beginning.

Part 1: Installing Nexus (Or Trying To)

I spun up a new DigitalOcean droplet—larger specs than the Java app server since Nexus requires more resources:

I SSH'd in, updated packages, and installed Java (Nexus requires Java to run):

sudo apt update && sudo apt upgrade -y
sudo apt install openjdk-17-jdk -y

Then I followed the Nexus installation guide:

# Download Nexus
cd /opt
sudo wget https://download.sonatype.com/nexus/3/latest-unix.tar.gz

# Extract
sudo tar -xvzf latest-unix.tar.gz

Nexus installs in two directories:

Creating a Dedicated Nexus User

Following the principle of least privilege (which I learned in Project 1), I created a dedicated user to run Nexus:

sudo adduser nexus
sudo chown -R nexus:nexus /opt/nexus /opt/sonatype-work/nexus3

Running Nexus as the Dedicated User

With the nexus user created and owning the directories, I switched to that user:

su - nexus

Then I started Nexus:

/opt/nexus/bin/nexus start

Output:

Starting nexus

I checked if it was running:

/opt/nexus/bin/nexus status

Output:

nexus is running.

Great! It said it was running. I opened my browser: http://143.198.XXX.XXX:8081

Connection refused.

Part 2: The Silent Failure

This is the most frustrating type of error: the silent failure. The nexus status command claimed it was running. But nothing was accessible.

I verified the process was actually running:

ps aux | grep nexus

I saw the nexus process in the list. So it was running... or was it?

I checked what was listening on port 8081:

sudo netstat -tulpn | grep 8081

Nothing. No output. No process was listening on that port.

So Nexus claimed to be running, showed up in the process list, but wasn't actually listening for connections. Something was failing silently.

I checked the logs in /opt/sonatype-work/nexus3/log/:

sudo tail -f /opt/sonatype-work/nexus3/log/nexus.log

The log file was... empty. No errors. No warnings. Just silence.

What do you do when there are no error messages?

Running Nexus in Foreground Mode

After an hour of frustrated Googling, I found a suggestion: run Nexus in foreground mode instead of as a background process. This forces it to output logs directly to the terminal instead of writing to log files.

First, I stopped the backgrounded instance:

/opt/nexus/bin/nexus stop

Then I ran it in foreground mode:

/opt/nexus/bin/nexus run

Immediately, I got output:

Exec Format Error
/opt/nexus/bin/nexus: /opt/nexus/bin/nexus: cannot execute binary file: Exec format error

The "Exec Format Error" Revelation

Exec format error means you're trying to run a binary that was compiled for a different CPU architecture than what your system has.

It's like taking a program built specifically for a new Apple Silicon Mac (M-series) and trying to run it on an older Intel Mac. The operating system might be exactly the same, but the underlying processor architecture speaks a completely different language, so the binary can't execute.

I checked my server's architecture:

uname -m

Output: x86_64

That's Intel/AMD architecture (also called amd64).

Then I looked at the filename of the Nexus tarball I'd downloaded: nexus-3.x.x-unix-aarch64.tar.gz

There it was.

aarch64 is ARM architecture—the same architecture used in Apple's M1/M2 chips, Raspberry Pi, and AWS Graviton instances. I had accidentally downloaded the ARM version of Nexus and was trying to run it on an x86_64 server.

Architecture Primer

For anyone coming from a Windows/Mac background where this distinction doesn't matter much:

x86_64 (Intel/AMD):

  • Used in most cloud servers (AWS EC2 general instances, DigitalOcean standard droplets)
  • Most common desktop/laptop processors
  • Also called "amd64" or "x64"

ARM (aarch64):

  • Used in modern Apple Silicon (M1/M2/M3), AWS Graviton, Raspberry Pi
  • More energy-efficient, increasingly common in cloud (Graviton is cheaper)
  • Completely different instruction set from x86

You cannot run an ARM binary on an x86 system or vice versa. The CPU literally doesn't understand the instructions.

Production lesson: Always verify architecture before downloading binaries. Use uname -m to check.

Part 3: The Correct Installation

I deleted everything and started over with the correct version:

# Remove the wrong version
sudo rm -rf /opt/nexus /opt/sonatype-work/nexus3

# Download the x86_64 (unix) version
cd /opt
sudo wget https://download.sonatype.com/nexus/3/latest-unix.tar.gz

# Extract and set up
sudo tar -xvzf latest-unix.tar.gz
sudo chown -R nexus:nexus /opt/nexus /opt/sonatype-work/nexus3

This time, when I ran Nexus in foreground mode:

sudo -u nexus /opt/nexus/bin/nexus run

Actual startup logs appeared:

-------------------------------------------------

Started Sonatype Nexus OSS 3.64.0-04

-------------------------------------------------

Success!

I accessed http://143.198.XXX.XXX:8081 and saw the Nexus UI.

First-Time Login

Nexus generates a random admin password on first startup and stores it in a file:

sudo cat /opt/sonatype-work/nexus3/admin.password

I copied that password, logged in as admin, and immediately changed it to something secure.

Security Note

After logging in, Nexus prompts you to configure anonymous access. Since this is a learning environment and not production, I enabled it for convenience.

Part 4: Understanding Nexus Architecture

Before configuring anything, I needed to understand how Nexus organizes artifacts. Coming from a background where "files go in folders," Nexus's structure took some getting used to.

The Hierarchy

Nexus
├── Blob Stores (physical storage)
│   ├── default (where files actually live on disk)
│   └── custom-blobs
├── Repositories (logical organization)
│   ├── maven-central (proxy to Maven Central)
│   ├── maven-snapshots (hosted - for dev builds)
│   ├── maven-releases (hosted - for prod builds)
│   └── maven-public (group - combines above)
└── Components (the actual artifacts)
    └── com.example.my-app:1.0-SNAPSHOT

Blob Stores: The actual filesystem location where artifacts are stored. Think of this as the "hard drive."

Repositories: Logical containers that determine access rules, versioning policies, and what kind of artifacts can be stored. Think of these as "databases" or "buckets."

Components: The actual JAR files, with metadata about group ID, artifact ID, and version.

Repository Types

Hosted: You upload artifacts here (your own builds)

Proxy: Caches artifacts from external repositories (like Maven Central)

Group: Combines multiple repositories into one URL

Why groups matter: Developers configure their build tools to point to ONE repository URL (the group). Nexus handles checking snapshots, then releases, then proxying to Maven Central if needed.

Part 5: Setting Up Users and Permissions

I wasn't going to use the admin account for publishing artifacts. That violates least privilege (again). Instead, I created a dedicated CI/CD user with limited permissions.

Creating a User

Settings → Security → Users → Create User

Understanding Nexus Roles and Privileges

Nexus has a granular permission system. A privilege is a single permission (like "read from repository X" or "write to repository Y"). A role is a collection of privileges.

I created a custom role ci-deploy-role with privileges to:

Specific Privileges Assigned:

Then I assigned this role to ci-user.

Why Not Just Use Admin?

If the CI/CD credentials get leaked, you want the blast radius contained. A compromised ci-user can only upload artifacts. A compromised admin can delete repositories, create users, and shut down Nexus entirely.

Part 6: Publishing from a Gradle Project

Now for the actual workflow: building an artifact locally and publishing it to Nexus.

Adding the Maven Publish Plugin

In build.gradle:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.5.5'
    id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.example'
version = '1.0-SNAPSHOT'
sourceCompatibility = 17

apply plugin: 'maven-publish'

The maven-publish plugin gives Gradle the ability to publish to Maven-format repositories. Even though this is a Gradle project, Maven and Gradle share the same artifact format for Java libraries.

Configuring the Publication

publishing{
    publications {
        maven(MavenPublication){
            artifact("build/libs/my-app-$version" + ".jar") {
                extension 'jar'
            }
        }
    }

    repositories {
        maven {
            name 'nexus'
            url "http://143.198.XXX.XXX:8081/repository/maven-snapshots/"
            allowInsecureProtocol = true
            credentials {
                username project.repoUser
                password project.repoPassword
            }
        }
    }
}

Key details:

  1. Targeting Snapshots: For this phase of the project, we are hardcoding the URL to point directly to our maven-snapshots repository since we are actively developing and testing the build.
  2. Credentials: Never hardcode usernames and passwords in build.gradle (that file gets committed to Git). Instead, use properties.
  3. allowInsecureProtocol: Required because I'm using HTTP instead of HTTPS. In production, you'd set up HTTPS with Let's Encrypt and remove this line.

Storing Credentials Safely

I created gradle.properties in the project root:

repoUser=ci-user
repoPassword=<actual-password>

Then immediately added it to .gitignore:

echo "gradle.properties" >> .gitignore

Why this matters: Git history is forever. Even if you delete a file later, the credentials remain in your commit history. GitHub scans for exposed credentials and will notify you (or worse, bots will find them and compromise your systems).

Then I ran the publish task:

gradle publish
Success
> Task :publish
Publishing com.example:my-app:1.0-SNAPSHOT
Uploading to nexus...
BUILD SUCCESSFUL in 3s

I went to Nexus UI → Browse → Components → maven-snapshots and there it was. First artifact published. ✅

Part 7: Publishing from a Maven Project

The workflow for Maven is similar but uses different configuration files.

Adding the Deployment Plugin

In pom.xml:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-deploy-plugin</artifactId>
            <version>3.1.1</version>
        </plugin>
    </plugins>
</build>

Configuring the Repository

Also in pom.xml:

<distributionManagement>
    <snapshotRepository>
        <id>nexus-snapshots</id>
        <url>http://143.198.XXX.XXX:8081/repository/maven-snapshots/</url>
    </snapshotRepository>
</distributionManagement>

Storing Credentials (Maven Way)

Maven stores credentials globally, not per-project. I created ~/.m2/settings.xml:

<settings>
    <servers>
        <server>
            <id>nexus-snapshots</id>
            <username>ci-user</username>
            <password><actual-password></password>
        </server>
    </servers>
</settings>

The <id> in settings.xml matches the <id> in pom.xml. That's how Maven connects them.

Deploying:

mvn package
mvn deploy

Part 8: The Blob Store Cleanup Surprise

Later that afternoon, after publishing several test artifacts, I wanted to clean up. I deleted some test components from the Nexus UI and checked disk space.

It hadn't changed.

Soft Deletes

Deleting artifacts in the Nexus UI doesn't actually delete them from disk. It only marks them as deleted in the database. This is a "soft delete."

Why? Nexus uses content-addressed storage. Multiple artifacts can reference the same underlying blob if they're identical files. If Nexus immediately deleted the blob when you deleted one artifact, other artifacts referencing that blob would break.

The Two-Phase Cleanup Process

To actually free disk space, you need to run two tasks:

  1. Cleanup Policy: Marks blobs as eligible for deletion.
  2. Compact Blob Store: Actually removes the marked blobs from disk.

After configuring a cleanup policy and running the "Compact blob store" task, I finally reclaimed 24 GB of space.

The Storage Metrics Issue

Even after compaction, the metrics in the UI might not update immediately because Nexus caches blob store statistics. You may need to run the "Rebuild blob store storage attributes" task to force a refresh.

Part 9: Understanding SNAPSHOT vs RELEASE Versioning

Throughout this project, I kept seeing references to "snapshots" and "releases" but didn't fully understand the distinction. Here's what I learned:

SNAPSHOT Versions

Format: 1.0-SNAPSHOT, 2.5-SNAPSHOT

Purpose: Development versions that are actively changing.

Behavior:

RELEASE Versions

Format: 1.0, 2.0, 1.5.3

Purpose: Stable, immutable versions for production.

Behavior:

What I Learned (The Real Takeaways)

1. Silent Failures Require Foreground Debugging

When a service claims to be running but isn't, don't trust systemd. Run the binary directly to see the error.

2. CPU Architecture Is Not Optional

I assumed "Linux" meant "works on any Linux." It doesn't. Binaries are compiled for specific CPU architectures, and x86_64 ≠ ARM.

3. Credentials Management Is a Spectrum

Hardcoding is bad. Local ignore files are okay for learning. Secrets managers (Vault, AWS Secrets) are mandatory for production.

4. Nexus Storage Is Not Intuitive

Deleting in the UI ≠ freeing disk space. You must understand the blob store compaction lifecycle.

5. RBAC Is Worth the Effort

Creating dedicated users with minimal permissions is annoying upfront but essential for security. It limits the blast radius if credentials are compromised.

6. Maven and Gradle Share More Than You Think

Both publish to Maven-format repositories. Both use the same artifact structure (groupId:artifactId:version). You can use one Nexus repository for projects using different build tools.

Production Considerations

This was a learning environment. In production, I'd add:

How This Connects to Real CI/CD

Right now, I'm publishing artifacts manually from my laptop. In a real environment:

Developer workflow:

  1. Developer pushes code to Git (GitHub, GitLab)
  2. CI server (Jenkins, GitHub Actions) detects the push
  3. CI builds the application (gradle build or mvn package)
  4. CI publishes the artifact to Nexus (gradle publish or mvn deploy)
  5. CI triggers deployment to dev environment

Deployment workflow:

  1. Deployment tool (Ansible, Terraform, Kubernetes) pulls artifact from Nexus
  2. Deploys exact version (e.g., 1.0) to target environment
  3. No building on deployment servers—just download and run

Benefits: Same artifact tested in dev/staging is deployed to prod (reproducibility). No "works on my machine" issues. Rollback is trivial.

Reflection

Time investment: ~4 hours
Cost: $48/month for the 8GB droplet (Nexus is hungry).

What I'd do differently: Verify architecture FIRST. Set up HTTPS from the start. Configure cleanup policies immediately, not as an afterthought.

What's Next

With Nexus running and artifacts being published, the next phase is containerization. I'll be learning Docker fundamentals, building images for the Java application, and pushing them to AWS ECR.

I'm expecting more networking issues and version conflicts, but that's where the real learning happens.

If you're working through similar projects or running into Nexus issues, feel free to reach out. I'm documenting everything as I go—mistakes, dead ends, and all.