Hướng dẫn tối ưu và chạy Sonatype Nexus 3 trên VPS 1 Gi RAM

05 tháng 4, 2026·11 phút đọc

Bài viết này chia sẻ cách tự_host kho Docker riêng tư và lưu trữ artifact bằng Sonatype Nexus 3 trên một chiếc VPS chỉ có 1 GiB RAM. Tìm hiểu về các cấu hình JVM, tối ưu hóa bộ nhớ, swap và Traefik để vận hành ổn định trong môi trường sản xuất.

Hướng dẫn tối ưu và chạy Sonatype Nexus 3 trên VPS 1 Gi RAM

Mỗi lần tôi đẩy một Docker image lên Docker Hub sử dụng gói miễn phí, tôi lại thường xuyên lo ngại về giới hạn tốc độ (rate limits), chính sách lưu giữ và sự phụ thuộc dần dần vào nhà cung cấp. Đối với các dự án cá nhân hoặc các nhóm nhỏ, việc tự_host một registry riêng tư mang lại nhiều lợi ích:

  • Không giới hạn tốc độ khi kéo (pull) image — điều cực kỳ quan trọng đối với các pipeline CI/CD.
  • Lưu trữ image riêng tư mà không cần trả phí cho các dịch vụ cloud registry.
  • Kho lưu trữ tập trung cho Docker images, npm packages, Maven artifacts và nhiều hơn nữa — tất cả dưới một mái nhà.
  • Kiểm soát hoàn toàn việc lưu giữ và truy cập.

Tuy nhiên, nhược điểm là Sonatype Nexus 3 là một ứng dụng Java. Nó được xây dựng cho các máy chủ doanh nghiệp, không phải cho các instance VPS giá rẻ. Tài liệu chính thức khuyến nghị tối thiểu 8 GiB RAM. Việc chạy nó trên 1 GiB thực sự là một bài toán kỹ thuật đầy thách thức về tài nguyên — và đó chính là loại bài toán tôi yêu thích.

Cài đặt và Cấu trúc

Hạ tầng

Thành phầnChi tiết
VPS1 vCPU, 1 GiB RAM, 25 GiB SSD
Hệ điều hànhUbuntu 22.04 LTS
Phiên bản Nexus3.90.2-alpine
Cơ sở dữ liệuPostgreSQL (bên ngoài, trên cùng host)
Reverse proxyTraefik v3 với TLS tự động
DNSrepository.bitnoises.com (UI), registry.bitnoises.com (Docker)

Kiến trúc

Kiến trúc hệ thống NexusKiến trúc hệ thống Nexus

Traefik sẽ xử lý tất cả việc kết thúc TLS. Nexus không bao giờ "thấy" lưu lượng HTTPS thô — nó chỉ giao tiếp qua HTTP đơn giản bên trong, giúp đơn giản hóa cấu hình considerably.

Vấn đề về Bộ nhớ

Dưới đây là bức tranh thực tế về ngân sách RAM của tôi trước khi Nexus khởi động:

Tổng RAM:              957 Mi
Hệ điều hành + kernel: ~150 Mi
Docker daemon:          ~50 Mi
Traefik:                ~30 Mi
PostgreSQL:             ~80 Mi
─────────────────────────────
Dành cho Nexus:   ~647 Mi

Nexus là một ứng dụng JVM. Dấu chân bộ nhớ của nó có hai thành phần chính:

  1. Heap (-Xmx) — cấp phát đối tượng, được quản lý bởi bộ thu gom rác (Garbage Collector).
  2. Direct memory (-XX:MaxDirectMemorySize) — bộ đệm ngoài heap, được sử dụng nhiều cho I/O.

Khuyến nghị mặc định của Sonatype là -Xmx2703m. Trên VPS của tôi, điều đó sẽ yêu cầu gấp 4 lần lượng RAM khả dụng. JVM sẽ ngay lập tức bắt đầu swap và container sẽ bị OOM-killed (kết thúc do thiếu bộ nhớ) trong vài phút.

Giải pháp ở đây là giảm quy mô một cách quyết liệt nhưng cẩn thận.

Tinh chỉnh JVM

-Xms128m                      # Bắt đầu với heap nhỏ, tăng khi cần
-Xmx384m                      # Giới hạn cứng của heap
-XX:MaxDirectMemorySize=192m  # Giới hạn bộ đệm off-heap
-XX:+UseG1GC                  # GC tốt hơn dưới áp lực bộ nhớ
-XX:MaxGCPauseMillis=300      # Mục tiêu thời gian dừng GC
-XX:G1HeapRegionSize=4m       # Vùng nhỏ hơn = lãng phí ít hơn
-XX:+UseStringDeduplication   # G1 loại bỏ chuỗi trùng lặp (~tiết kiệm 5-10% heap)
-XX:SoftRefLRUPolicyMSPerMB=0 # Xóa tham chiếu mềm tích cực khi áp lực

Thông tin quan trọng là tách biệt -Xms-Xmx. Bắt đầu ở mức 128m có nghĩa là JVM tiêu thụ tối thiểu RAM khi khởi động và chỉ mở rộng heap khi Nexus thực sự cần. Trên một registry cá nhân khá yên tĩnh, hiếm khi nó cần mở rộng quá 250–300m trong thực tế.

Tổng dấu chân JVM: ~680m. Giới hạn mem_limit của Docker được đặt ở mức 700m, để lại 20m bộ đệm cho sự biến đổi của JVM.

Tầm quan trọng của Swap

Tuyệt đối không chạy ứng dụng JVM trên máy không có swap.

Swap trên SSD không phải là một chiến lược hiệu suất — nó là một tấm lưới an toàn. Nếu không có nó, Linux OOM killer sẽ chấm dứt container của bạn ngay lập tức khi nó vượt quá giới hạn bộ nhớ, không có cảnh báo và không có tắt xuống an toàn. Nexus không xử lý tốt việc bị giết đột ngột; bạn có nguy cơ làm hỏng cơ sở dữ liệu hoặc blob store ở trạng thái không nhất quán.

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

# Chỉ sử dụng swap như một giải pháp cuối cùng
echo 'vm.swappiness=10' >> /etc/sysctl.conf
sysctl -p

Với vm.swappiness=10, Linux ưu tiên mạnh mẽ việc giữ dữ liệu trong RAM và chỉ swap ra file khi thực sự áp lực. SSD chịu một chút tác động nhỏ, nhưng dịch vụ của bạn sẽ sống sót qua các đỉnh (spikes).

Sau khi thêm swap, bức tranh bộ nhớ của tôi trông như sau:

$ free -h
               total   used    free    buff/cache  available
Mem:           957Mi   194Mi   91Mi    671Mi       594Mi
Swap:          2.0Gi   125Mi   1.9Gi

Cột buff/cache (671Mi) trông có vẻ đáng báo động nhưng thực tế không phải vậy — Linux sử dụng RAM trống làm đĩa cache, và cache đó có thể bị loại bỏ ngay lập tức khi Nexus cần. Cột available (594Mi) mới là con số quan trọng.

File cấu hình compose.yml

services:
  nexus:
    image: sonatype/nexus3:3.90.2-alpine
    container_name: nexus
    restart: unless-stopped
    user: "200:200"
    environment:
      INSTALL4J_ADD_VM_PARAMS: >-
        -Xms128m
        -Xmx384m
        -XX:MaxDirectMemorySize=192m
        -XX:+UseG1GC
        -XX:MaxGCPauseMillis=300
        -XX:G1HeapRegionSize=4m
        -XX:+UseStringDeduplication
        -XX:SoftRefLRUPolicyMSPerMB=0
        -Djava.util.prefs.userRoot=/nexus-data/javaprefs
        -Dnexus.datastore.enabled=true
        -Dnexus-ssl-proxy=true
      NEXUS_DATASTORE_NEXUS_JDBCURL: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
      NEXUS_DATASTORE_NEXUS_USERNAME: ${DB_USER}
      NEXUS_DATASTORE_NEXUS_PASSWORD: ${DB_PASSWORD}
    volumes:
      - "./data:/nexus-data"
    mem_limit: 700m
    memswap_limit: 1400m      # 700m RAM + 700m swap headroom
    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://localhost:8081/service/rest/v1/status || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 180s      # Nexus khởi động chậm trên cấu hình thấp
    networks:
      - traefiknetwork
      - infra
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefiknetwork"
      # UI
      - "traefik.http.routers.nexus-ui.rule=Host(`${NEXUS_HOST}`)"
      - "traefik.http.routers.nexus-ui.entrypoints=websecure"
      - "traefik.http.routers.nexus-ui.tls=true"
      - "traefik.http.routers.nexus-ui.tls.certresolver=letsencrypt"
      - "traefik.http.routers.nexus-ui.service=nexus-ui"
      - "traefik.http.services.nexus-ui.loadbalancer.server.port=8081"
      # Docker registry
      - "traefik.http.routers.nexus-docker.rule=Host(`${REGISTRY_HOST}`)"
      - "traefik.http.routers.nexus-docker.entrypoints=websecure"
      - "traefik.http.routers.nexus-docker.tls=true"
      - "traefik.http.routers.nexus-docker.tls.certresolver=letsencrypt"
      - "traefik.http.routers.nexus-docker.service=nexus-docker"
      - "traefik.http.services.nexus-docker.loadbalancer.server.port=5000"
      - "traefik.http.middlewares.docker-headers.headers.customrequestheaders.Docker-Distribution-Api-Version=registry/2.0"
      - "traefik.http.middlewares.nexus-docker-buffering.buffering.maxRequestBodyBytes=0"
      - "traefik.http.routers.nexus-docker.middlewares=docker-headers,nexus-docker-buffering"

networks:
  traefiknetwork:
    external: true
  infra:
    external: true

Một số điểm đáng lưu ý

user: "200:200" — Ảnh Nexus nội bộ chạy dưới UID 200. Việc đặt rõ ràng điều này ngăn chặn việc thực thi ngẫu nhiên dưới quyền root. Thư mục ./data phải được sở hữu trước: sudo chown -R 200:200 ./data.

start_period: 180s — Nếu không có cái này, Docker sẽ đánh dấu container là không khỏe (unhealthy) trước khi nó hoàn tất khởi động, điều này có thể kích hoạt vòng lặp khởi động lại. Trên phần cứng hạn chế, Nexus mất 2–3 phút để khởi động.

maxRequestBodyBytes=0 — Label Traefik quan trọng nhất cho Docker registry. Nếu không có nó, việc đẩy bất kỳ lớp image nào lớn hơn giới hạn kích thước body mặc định của Traefik (2m) sẽ thất bại với lỗi 413 khó hiểu.

PostgreSQL vs Database tích hợp

Nexus 3 hỗ trợ hai backend cơ sở dữ liệu: H2 tích hợp và PostgreSQL bên ngoài. Tôi chọn PostgreSQL vì vài lý do:

  • Độ tin cậy — H2 ổn định cho phát triển, nhưng tôi đã thấy nó bị hỏng khi JVM bị kill đột ngột. PostgreSQL xử lý việc tắt xuống bẩn một cách nhẹ nhàng hơn.
  • Khả năng quan sát — Tôi có thể truy vấn trực tiếp cơ sở dữ liệu để kiểm tra trạng thái, chạy sao lưu và giám sát số lượng kết nối.
  • Tính nhất quán — PostgreSQL đang chạy trên hạ tầng của tôi cho các dịch vụ khác. Một phần di chuyển ít hơn.

Sự đánh đổi: không có file admin.password trên cài đặt dựa trên PostgreSQL. Thông tin đăng nhập mặc định đơn giản là admin / admin123, và Nexus buộc thay đổi mật khẩu khi đăng nhập lần đầu.

Cấu hình Docker Registry

Sau khi đăng nhập lần đầu:

Bật Bearer Token Realm

AdministrationSecurityRealms → chuyển Docker Bearer Token Realm sang Active.

Đây là nguyên nhân phổ biến nhất của lỗi 401 Unauthorized khi sử dụng docker login. Nó phải được bật.

Tạo Repository

AdministrationRepositoryRepositoriesCreate repositorydocker (hosted)

Đặt cổng HTTP thành 5000 (đây là cổng Traefik sẽ route tới), bỏ chọn HTTPS (Traefik sẽ xử lý việc đó), và đặt chính sách triển khai theo sở thích của bạn.

Tích hợp CI/CD

Tôi đã tạo một người dùng ci chuyên dụng với vai trò tối thiểu, tuân theo nguyên tắc đặc quyền tối thiểu. Vai trò này chỉ có các đặc quyền cần thiết để đẩy và kéo từ repository Docker — không có quyền admin, không có quyền truy cập các repository khác.

Trong GitLab CI/CD:

stages:
  - push

push-alpine-to-nexus:
  stage: push
  image: docker:29.2.1
  services:
    - docker:29.2.1-dind

  variables:
    DOCKER_TLS_CERTDIR: ""

  script:
    # Đăng nhập vào private registry
    - echo "$NEXUS_PASSWORD" | docker login registry.bitnoises.com \
        --username "$NEXUS_USER" \
        --password-stdin

    # Kéo image Alpine từ Docker Hub
    - docker pull alpine:latest

    # Gán thẻ (tag) image cho private registry
    - docker tag alpine:latest registry.bitnoises.com/alpine:latest

    # Đẩy image lên Nexus
    - docker push registry.bitnoises.com/alpine:latest

Sử dụng commit SHA làm thẻ image thay vì latest mang lại cho bạn khả năng truy xuất đầy đủ — mọi lần triển khai đều có thể truy ngược lại một commit chính xác.

Quản lý ổ cứng

SSD là ràng buộc khác. 25 GiB nghe có vẻ nhiều cho đến khi bạn bắt đầu lưu trữ Docker images — một image ứng dụng Node.js điển hình là 200–400 MiB, và bạn tích lũy nhiều phiên bản rất nhanh.

Chính sách dọn dẹp (Cleanup Policies)

Trong AdministrationRepositoryCleanup Policies, tôi đã tạo một chính sách loại bỏ:

  • Các thành phần cũ hơn 30 ngày
  • Các thành phần không được tải xuống trong 14 ngày

Đính kèm vào repository docker-hosted và lên lịch là một tác vụ hàng tuần, điều này giữ cho blob store không phát triển vô hạn.

Bài học kinh nghiệm

Bộ nhớ JVM không chỉ là Heap. Nhiều hướng dẫn nói "đặt -Xmx bằng một nửa RAM của bạn" mà không đề cập đến bộ nhớ trực tiếp, metaspace, code cache, hoặc stack luồng JVM. Dấu chân thực tế là heap + direct + ~100m nội bộ của JVM. Hãy ngân sách cho tất cả.

Swap trước mọi thứ khác. Tôi gần như triển khai mà không có swap vì "swap trên SSD chậm". Swap trên SSD với swappiness 10% thực sự hiếm khi được sử dụng trong hoạt động bình thường, nhưng nó đã cứu tôi khỏi OOM kills nhiều lần trong quá trình khởi động Nexus khi áp lực bộ nhớ cao nhất.

Thứ tự label Traefik rất quan trọng. Label middlewares phải tham chiếu đến tên middleware được định nghĩa trong các label khác trong cùng dịch vụ. Nếu bạn định nghĩa docker-headersnexus-docker-buffering nhưng chỉ tham chiếu một cái trong label router, cái kia sẽ âm thầm không hoạt động.

start_period không phải tùy chọn cho dịch vụ chậm. start_period của healthcheck Docker là thời gian chờ ân hạn trước khi các lần kiểm tra thất bại được tính vào retries. Đối với dịch vụ mất 2–3 phút để khởi động, việc đặt cái này thành 30s có nghĩa là Docker sẽ khởi động lại container trước khi nó thậm chí đã bắt đầu xong — tạo ra một vòng lặp khởi động lại vô hạn trông giống như vấn đề bộ nhớ.

Thông tin đăng nhập mặc định của PostgreSQL không nằm trong logs. Đến từ Nexus sử dụng H2 nơi một file admin.password được tạo ra, điều này đã làm tôi bất ngờ. Cài đặt dựa trên PostgreSQL chỉ đơn giản sử dụng admin / admin123 mà không có file hoặc mục log nào chỉ ra điều này.

Kết quả

Một Docker registry và artifact store riêng tư đầy đủ chức năng, chạy đáng tin cậy trên phần cứng chỉ tốn vài euro một tháng. Việc sử dụng bộ nhớ ở trạng thái ổn định:

$ docker stats nexus --no-stream
CONTAINER   CPU %   MEM USAGE / LIMIT   MEM %
nexus       0.3%    412MiB / 700MiB     58.9%

58% sử dụng bộ nhớ ở trạng thái rảnh, với 42% dung lượng trống trước khi chạm giới hạn cứng và 700m swap khả dụng ngoài ra. Đối với một dự án hạ tầng cá nhân, đó là một biên độ an toàn thoải mái.

Toàn bộ cấu hình, script và tài liệu có sẵn trên GitHub: gitlab.com/hanatole/nexus

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗