在Ubuntu24上部署JupyterHub + JupyterLab + DockerSpawner

在 Ubuntu 系统上部署 JupyterHub、JupyterLab 和 DockerSpawner 的详细方案,涵盖环境准备、安装、配置和部署步骤。目标是创建一个多用户 Jupyter 环境,每个用户通过 DockerSpawner 运行独立的 JupyterLab 容器,确保隔离性和可扩展性。本方案基于官方文档和社区最佳实践,也适用于 Ubuntu 20.04 或 22.04。

总体上分两种情况,一种是直接在服务器上启动JupyterHub,另一种是在docker中启动JupyterHub,后者隔离效果更好,推荐使用,但是绑定服务器目录时需要注意。以下方案中会作出区分说明。


1. 环境准备

1.1 系统要求

  • 操作系统:Ubuntu 20.04 LTS 或 22.04 LTS(推荐服务器版)。
  • 硬件要求:
    • 最低配置:2核CPU,4GB内存,20GB磁盘空间。
    • 推荐配置:4核CPU,8GB内存,50GB+磁盘空间(根据用户数量和数据存储需求调整)。
  • 网络:确保服务器可以访问互联网以拉取 Docker 镜像和安装依赖。
  • 权限:需要 root 或 sudo 权限。

1.2 安装基本工具

更新系统并安装必要的工具:

bash

sudo apt update && sudo apt upgrade -y

下面这一步并非必要,主要用于在服务器上直接启动JupyterHub。

bash

sudo apt install -y curl git python3 python3-pip python3-venv

1.3 安装 Docker

安装 Docker 和 Docker Compose,用于运行 JupyterHub 和用户容器:

bash

# 安装 Docker
sudo apt install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker

# 验证 Docker 安装
docker --version

# 安装 Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# 验证 Docker Compose 安装
docker-compose --version

# 将当前用户添加到 docker 组(避免每次使用 docker 需要 sudo)
sudo usermod -aG docker $USER
newgrp docker

上面这一步需要重新登录或者重启才能生效。其中docker-compose也可以不装,新版的docker中自带docker compose命令。

2. 安装 JupyterHub 和依赖(这是服务器直接部署JupyterHub,使用docker的话可以跳过)

2.1 创建虚拟环境

为了隔离 JupyterHub 的 Python 依赖,创建一个虚拟环境:

bash

python3 -m venv /opt/jupyterhub
source /opt/jupyterhub/bin/activate

2.2 安装 JupyterHub 和 DockerSpawner

在虚拟环境中安装 JupyterHub 和相关包:

bash

pip install --upgrade pip
pip install jupyterhub jupyterlab dockerspawner
  • jupyterhub:核心服务,用于管理多用户环境。
  • jupyterlab:为用户提供 JupyterLab 界面。
  • dockerspawner:用于在 Docker 容器中启动单用户 JupyterLab 实例。

2.3 安装 Node.js(可选,JupyterLab 依赖)

JupyterLab 需要 Node.js 来支持扩展和前端功能:

bash

sudo apt install -y nodejs npm

3. 配置 JupyterHub(这个配置文件是用于服务器直接部署,docker部署的话其中的设置有所区别)

3.1 创建 JupyterHub 配置文件

生成默认配置文件(也可以跳过,直接复制下面的配置文件内容进去,默认配置文件很长,看起来不方便):

bash

mkdir /opt/jupyterhub
jupyterhub --generate-config -f /opt/jupyterhub/jupyterhub_config.py

配置文件位于 /opt/jupyterhub/etc/jupyterhub_config.py,以下是关键配置步骤。

3.2 配置 DockerSpawner

编辑 jupyterhub_config.py,添加以下内容以启用 DockerSpawner 和 JupyterLab:

python

c = get_config()
import os
import sys
from dockerspawner import DockerSpawner

# 设置 DockerSpawner
c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
c.DockerSpawner.image = 'jupyter/minimal-notebook:x86_64-python-3.11.6'
c.DockerSpawner.network_name = 'jupyterhub-network'

# 设置 JupyterLab 作为默认界面
c.Spawner.default_url = '/lab'

# 持久化存储配置
notebook_dir = '/home/jovyan/work'
c.DockerSpawner.notebook_dir = notebook_dir
c.DockerSpawner.volumes = { 
    '/mnt/raid/jupyterhub_persistent/{username}': notebook_dir,
    '/mnt/raid/jupyterhub_shared': '/home/jovyan/shared'
}
# 上面路径中的/mnt/raid是我自己服务器的一个地址,可以替换。注意这个地址不管在服务器直接运行JupyterHub还是在docker中,都是设置为服务器上的地址,因为是绑定给用户容器的。

# 动态创建用户目录
def create_dir_hook(spawner):
    username = spawner.user.name
    volume_path = os.path.join('/mnt/raid/jupyterhub_persistent', username)
    if not os.path.exists(volume_path):
        os.mkdir(volume_path, 0o755)
        os.chown(volume_path, 1000, 100)  # jovyan 用户的 UID 和 GID
c.Spawner.pre_spawn_hook = create_dir_hook

# 资源限制
c.DockerSpawner.extra_host_config = {
    'mem_limit': '16g',  # 每个容器 64GB,10 个用户平分 640GB
    'cpu_period': 100000,
    'cpu_quota': 1000000,  # 每个容器 1 核心,10 个用户共 10 核心
}

# 如果本地端口有冲突可以设置,否则不设置
c.JupyterHub.bind_url = 'http://:8090'
c.JupyterHub.hub_connect_ip = '127.0.0.1'
c.JupyterHub.hub_ip = '0.0.0.0'
c.JupyterHub.hub_port = 9000

# 可选:设置认证,根据需要选择认证方式,建议用这种自定义用户比较方便,不用系统的用户
c.JupyterHub.authenticator_class = 'nativeauthenticator.NativeAuthenticator'
c.NativeAuthenticator.enable_signup = True
c.NativeAuthenticator.open_signup = True
c.Authenticator.admin_users = {'admin'}

# 可选:自动停止闲置服务器
c.JupyterHub.services = [
#    {
#        'name': 'cull_idle',
#        'admin': True,
#        'command': 'python3 /opt/jupyterhub/cull_idle_servers.py --timeout=3600'.split(),
#    },
    {
        "name": "jupyterhub-idle-culler-service",
        "command": [
            sys.executable,
            "-m", "jupyterhub_idle_culler",
            "--timeout=3600",
        ],
         "admin": True,
    }
]

3.3 创建 Docker 网络

为 JupyterHub 和用户容器创建一个专用的 Docker 网络:

bash

docker network create jupyterhub-network

3.4 配置认证

JupyterHub 默认使用 PAM 认证(基于系统用户)。若需要简单测试,可以配置 Native Authenticator 允许用户自注册:

bash

pip install jupyterhub-nativeauthenticator

在 jupyterhub_config.py 中添加:

python

c.JupyterHub.authenticator_class = 'nativeauthenticator.NativeAuthenticator'
c.Authenticator.admin_users = {'admin'}  # 设置管理员用户

若使用其他认证方式(如 OAuth、LDAP),请参考 JupyterHub 官方文档配置对应 Authenticator。

3.5 数据持久化

为确保用户数据持久化,DockerSpawner 使用 Docker 卷(如上配置)。每个用户的卷命名为 jupyterhub-user-{username},存储在 /home/jovyan/work 目录。

若需要共享数据目录,可以挂载主机路径:

python

c.DockerSpawner.volumes = {
    'jupyterhub-user-{username}': notebook_dir,
    '/path/on/host/shared': '/home/jovyan/shared'
}

确保主机路径权限正确:

bash

sudo chown :1000 /path/on/host/shared
sudo chmod g+rws /path/on/host/shared
sudo setfacl -d -m g::rwx /path/on/host/shared

4. 使用 Docker Compose 部署

为简化管理,使用 Docker Compose 部署 JupyterHub 和相关服务。

4.1 创建 docker-compose.yml

在 /opt/jupyterhub 目录下创建 docker-compose.yml:

yaml

version: '3.8'
services:
  jupyterhub:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: jupyterhub
    restart: unless-stopped
    ports:
      - "9000:8000"
    volumes:
      - /opt/jupyterhub/srv:/srv/jupyterhub # 这里单独设置了一个srv以跟docker中的srv对应,区别于服务器直接部署的配置,测试时方便区分
      - /var/run/docker.sock:/var/run/docker.sock
      - /mnt/raid/jupyterhub/jupyterhub_persistent:/jupyterhub/jupyterhub_persistent
      - /mnt/raid/jupyterhub/jupyterhub_shared:/jupyterhub/jupyterhub_shared
    environment:
      - DOCKER_HOST=unix:///var/run/docker.sock
      - DOCKER_NOTEBOOK_IMAGE=jupyter/scipy-notebook:latest
      - DOCKER_NETWORK_NAME=jupyterhub-network
    networks:
      - jupyterhub-network

networks:
  jupyterhub-network:
    external: true

用户环境定制

若需要自定义用户镜像,创建 Dockerfile:

因为启用了nativeauthenticator所以默认的镜像不行,需要自己通过Dockerfile加上。同时也指定自己的jupyterhub_config

Dockerfile

# Use the official JupyterHub base image
FROM jupyterhub/jupyterhub:latest

# Install dockerspawner and nativeauthenticator
RUN pip install --no-cache-dir \
    dockerspawner \
    jupyterhub-nativeauthenticator \
    jupyterhub-idle-culler

# Create directory for persistent storage (if needed inside the hub container)
RUN mkdir -p /jupyterhub/jupyterhub_persistent /jupyterhub/jupyterhub_shared
RUN chmod -R 755 /jupyterhub

# Copy the jupyterhub_config.py into the container
COPY jupyterhub_config.py /srv/jupyterhub/jupyterhub_config.py

# Expose the default JupyterHub port
EXPOSE 8000

# Set working directory
WORKDIR /srv/jupyterhub

# Start JupyterHub
CMD ["jupyterhub", "-f", "/srv/jupyterhub/jupyterhub_config.py"]

4.2 用docker部署并启动 JupyterHub(这是方式二)

bash

cd /opt/jupyterhub
docker-compose up -d

4.3 验证部署

  • 访问 http://<服务器IP>:8000,应看到 JupyterHub 登录页面。
  • 使用管理员账户(如 admin)登录,测试创建用户和启动 JupyterLab 容器。
  • 检查 Docker 容器:bashdocker ps应看到 jupyterhub 容器和用户容器(如 jupyter-<username>)。

4.4 日志查看

若遇到问题,查看日志:

bash

docker logs jupyterhub

5. 高级配置

5.1 HTTPS 配置(不建议,建议另外通过caddy代理并设置ssl)

为生产环境,建议配置 HTTPS:

  1. 使用 Let’s Encrypt 获取免费 SSL 证书:bashsudo apt install -y certbot python3-certbot-nginx certbot certonly --standalone -d <your-domain>
  2. 在 jupyterhub_config.py 中配置 SSL:pythonc.JupyterHub.ssl_key = '/etc/letsencrypt/live/<your-domain>/privkey.pem' c.JupyterHub.ssl_cert = '/etc/letsencrypt/live/<your-domain>/fullchain.pem' c.JupyterHub.port = 443
  3. 更新 docker-compose.yml,将端口改为 443:443。

5.2 GPU 支持

若需要 GPU 支持,确保主机安装了 NVIDIA 驱动和 nvidia-container-toolkit:

bash

sudo apt install -y nvidia-driver-<version> nvidia-container-toolkit

在 jupyterhub_config.py 中添加:

python

c.DockerSpawner.extra_host_config = {'runtime': 'nvidia'}

使用支持 GPU 的镜像,如 jupyter/tensorflow-notebook。

5.3 用户环境定制

若需要自定义用户镜像,创建 Dockerfile:

dockerfile

FROM jupyter/scipy-notebook:latest
RUN pip install numpy pandas matplotlib
CMD ["jupyterhub-singleuser"]

构建并推送到 Docker Hub 或本地仓库:

bash

docker build -t my-jupyterlab-image .
docker tag my-jupyterlab-image <your-dockerhub-username>/my-jupyterlab-image:latest
docker push <your-dockerhub-username>/my-jupyterlab-image:latest

在 jupyterhub_config.py 中更新:

python

c.DockerSpawner.image = '<your-dockerhub-username>/my-jupyterlab-image:latest'

6. 常见问题排查

  • “Spawn failed” 错误:
    • 检查 docker logs jupyterhub 是否有 KeyError 或权限问题。
    • 确保 Docker 网络 jupyterhub-network 已创建。
    • 验证 DOCKER_NOTEBOOK_IMAGE 是否可拉取。
  • 用户容器无法访问数据库:
    • 确保数据库容器在同一 Docker 网络中:bashdocker network connect jupyterhub-network <database-container>
  • 权限问题:
    • 检查卷挂载路径权限,确保用户 jovyan(UID 1000)有写权限。
  • JupyterLab 不显示:
    • 确认 c.Spawner.default_url = ‘/lab’ 已设置。
    • 确保镜像中已安装 JupyterLab:bashdocker run -it <image> pip show jupyterlab

7. 维护与备份

  • 备份用户数据:
    • 用户数据存储在 Docker 卷中,查看卷:bashdocker volume ls
    • 备份卷:bashdocker run --rm -v jupyterhub-user-<username>:/data -v /backup:/backup busybox tar cvf /backup/user-<username>.tar /data
  • 更新 JupyterHub:
    • 停止服务:docker-compose down
    • 更新镜像:docker pull jupyterhub/jupyterhub:latest
    • 重新启动:docker-compose up -d
  • 清理无用容器:bashdocker container prune

8. 参考资源


通过以上步骤,您可以在 Ubuntu 上成功部署一个支持 JupyterLab 和 DockerSpawner 的 JupyterHub 环境,适合教学、科研或团队协作场景。如需进一步定制或遇到具体问题,请提供更多细节,我可以为您提供针对性指导!

个人博客服务器从Ubuntu16升级到了Ubuntu22

因为阿里云的ECS开了性能限制,博客有几个访问就挂掉,索性整体迁移一下系统,于是把dnmp整个文件夹备份到本地,然后停机,换镜像,启动,再scp把文件放回去(这里其实应该打包下载和上传,会快很多)。然后用dnmp里面的一键启动命令恢复环境,最后docker compose up -d启动网站。

不过这中间因为周末玩耍耽误了几天,幸好这个博客也没有多少人。

岳父的慢阻肺

岳父大人的慢阻肺一直是一个高危因素,自过年加重一次之后一直没有恢复,走走路就掉氧浓度,不到90%,吃了衡水中医院的中药也不见起色,于是约了中日友好医院的床位,等着来个全面检查。期间,到了广安门中医院开了中药,大夫看到他的现状对其是否需要用制氧机和呼吸机做氧疗有所怀疑,似乎没有那么重。等到在中日友好医院住了几天,做了肺功能和其他各项检查,终于给岳父确诊了糖尿病,并调整了心脏的药,把他的血糖和心律控制下来了,于是血氧也很神奇的转好了,这着实让岳父感到意外,我们也是。

于是,我又一次对地方医院和北京重点医院的诊疗水平差距有了进一步的认识。看上去似乎没有特别的手段,但是北京的大夫规范细致的诊治确实有效,而衡水的大夫虽然救了急,却对具体的康复方法没有任何指导。

有一个典型的例子,用于治疗慢阻肺的吸入药用到最好的,衡水也是这么开的,但是吸入方法却没有告知,也没有观察用的对不对,在北京问了之后,才发现之前几乎是无效吸入。而血压对血氧的影响之大,也不在之前的感知之内。

在我想象中,这些都是照手册就能学会的东西,医生应该完全可以自学掌握。

慢阻肺如何选择医用制氧机来进行长期氧疗

本文仅以个人非医学专业了解到的产品和医疗信息分享,不能替代医学治疗方案。

据医嘱,吸氧需要达到4L/min的流量才有效,但这个指标的前提是制氧机或者气瓶出氧的浓度要在90%以上,虽然国标要求医用制氧机必须达到90%,而目前市场上的制氧机普遍宣称符合这个标准,但从评论以及个人非严格检测来看,这个标准似乎没有那么容易实现。

这里列出制氧机的几个关键指标,请逐一核实:

  1. 可连续工作时间,这个涉及制氧机的可用性和耐用性,有的声称达到72小时或96小时,关键要看说明书上是否有明文说明,不能只看商品标题或视频,有的宣称7*24小时,目前来看,多半是虚假宣传,因为目前几千元的制氧机显然达不到7天连续工作。
  2. 出氧浓度,国家标准要求90%以上,有的机器标>=90%,有的标93%(+-3%),但有的机器会有附加说明,比如开机15分钟才能达到这个浓度(因内部有储气罐,可能需要排空一段时间),我买了一台欧姆龙(安徽迈睿思代工),前几次开机数分钟后出氧浓度基本上只在85%左右,最高89,但机器自身的氧气浓度一直显示为95%,我在12315上做了投诉,不知道接下来多久会有回复。还有的机器并不是在任何流量下都是90%以上,比如有的机器在详细描述中会补充说只在某个低流量区间能达到这个浓度,再大了相当于就是多吹点空气了,也难怪有的制氧机宣称不做空气的搬运机。我前些年买的那个鱼跃的流量控制显示是1L~5L,现在仔细查才知道其实是3L的机器,调到5L也没有多大意义,而现在实测出氧只有50%,所以买个新的。欧姆龙这个因为是周末提交的投诉,12315无反馈,退货申请暂时没有通过,但后来开机测试能达到93%(机身还是显示95%),就先用着了。
  3. 机器工作噪音,这个也挺关键的,在欧姆龙那台出氧浓度不达标的情况下,我又下单了一台鱼跃的6L机器,但下单后仔细再看评论,很多视频评论反馈机器噪音大,看了下他的说明书,是小于60分贝,这个确实高了,从视频里看,也确实比手上这台欧姆龙大得多。看网上的商品资料,有的说在29分贝,有的说在36分贝,能接受哪一种,还是要慎重考虑。
  4. 能否退货,从欧姆龙和鱼跃的评论区看,普遍退货困难,有的是因为噪音大,有的因为出氧浓度不达标,但都是因为开机过,不能退货,但这东西不开机怎么知道怎么样呢?所以有的评论总结不如在线下买,可以当场试机,不知道厂家是否针对线上有特殊政策。这类产品质量不稳定的现实应该是可以确认的,这种特殊商品如何退货还需要更清晰的政策和规则,有的商品明确说明只要一次性使用的附件没有拆封就可以退货,比如吸氧管之类的,这算比较合理的。
  5. 分子筛是否进口,尽管这玩意近些年国产化做的很到位,但从产品定价来看,一两千的产品都用国产的,四五干就出现用进口的了,显然,国产的还是达不到进口产品的水平。

测试成本很高,自己搞不定,网购的氧气检测仪也不一定精度就很高,但如果氧气浓度检测仪和制氧机自身的氧气浓度值都不可靠,那还有什么可靠的产品呢?

Vue3+VueUse 极简实现可拖拽侧边栏

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useMousePressed, useMouseInElement } from '@vueuse/core'

const containerRef = shallowRef()
const dividerRef = shallowRef()

const { pressed } = useMousePressed({ target: dividerRef, touch: false })
const { elementX } = useMouseInElement(containerRef)

const asideWidth = ref('300px')

watch(elementX, (newVal) => {
  console.log(newVal, pressed.value)
  if (!pressed.value) return
  if (newVal < 300) {
    asideWidth.value = '300px'
    return
  }
  asideWidth.value = `${Math.floor(newVal)}px`
})

</script>

<template>
<ElContainer v-show="panel === 'tree'" ref="containerRef" class="container">
      <ElAside class="aside"
        ></ElAside>
      <div ref="dividerRef" class="divider"></div>
      <ElMain class="main">
      </ElMain>
 </ElContainer>
<ElContainer v-show="panel === 'card'"> <BookCard :book="currentBook" /></ElContainer>
</template>
<style scoped lang="less">
.aside {
  width: v-bind('asideWidth');
}
.divider {
  flex: 0 0 4px;
  &:hover {
    background-color: var(--el-border-color);
    cursor: col-resize;
  }
  &:active {
    background-color: var(--el-border-color);
    cursor: col-resize;
  }
}
</style>

在flex容器(container)中横向排布侧边栏(aside)、分隔线(divider)和主要区域(main)通过useMousePressed获取分隔线上的鼠标按压状态,通过useMouseInElement获取容器内的鼠标移动状态,这些状态是响应式的侦听鼠标横向移动距离,仅当鼠标在分隔线按下(即拖拽)时,同步修改侧边栏宽度。

参考:流烨(链接:https://juejin.cn/post/7384242126429405225),相比调整了一下动态设置宽度的方式,改为width: v-bind(‘asideWidth’),同时,优化.divider的样式,增加hover,让它平时隐藏,鼠标滑过时显示更容易寻找。

l5-swagger如何设置服务端路径

正如tests/storage/annotations/OpenApi/L5SwaggerAnnotationsExampleServer.php所示:

<?php

namespace Tests\storage\annotations\OpenApi;

/**
 *  @OA\Server(
 *      url=L5_SWAGGER_CONST_HOST,
 *      description="L5 Swagger OpenApi dynamic host server"
 *  )
 *
 *  @OA\Server(
 *      url="https://projects.dev/api/v1",
 *      description="L5 Swagger OpenApi Server"
 * )
 */
class L5SwaggerAnnotationsExampleServer
{
}

L5_SWAGGER_CONST_HOST是作为一个变量直接引用的,这个变量在.env中设置,在实际项目中,尤其是多实例部署的产品,优先考虑用这种方法,而其中第二个是直接写死的地址,这种更适合相对固定的内部测试或者官方接口站点作为对照。实际上,api/doc还会依据L5_SWAGGER_BASE_PATH自动加载一个相对的接口地址,这个更适合用在调试环境,启动端口有可能会变,而且多个开发者的配置也不一定一致。L5_SWAGGER_CONST_HOST和L5_SWAGGER_BASE_PATH之间是相对独立的,可以搜索一下l5-swagger源代码看看逻辑,可惜这一点在l5-swagger的文档中说的并不清楚。

出版行业如何选择大模型

过年期间deepseek吵得很热闹,不过就我看到的信息,更像是一次冲喜,毕竟要过除夕了。我不太相信惊喜,二十年前在武汉的某博士沙龙上我突发灵感得到的结论,一切惊喜都可视作异常。回京后,做了下简单的测试,拿一个简单地问题去问这几个模型,这个问题还不算很苛刻,只是想看看训练时的语料审查有多么严重。

按结果的省略程度排序依次是:

  1. deepseek
  2. qwen
  3. mistral

拿出版行业来说,尽管审查也是一个很重的任务,但这个任务是由编辑来承担的,大模型没有权力直接来操刀,否则很容易造成失真,编辑就没法干活了。即使有deepseek无审查版也不行,因为那个无审查只是推理时无审查,并不能解决训练阶段的语料审查。

所以,这个可以当作出版行业大模型的一个选择条件,如果是2C我没有什么意见,安全第一,但是从编辑专业角度而言,必须选择一个中立的大模型来作为基础模型。这几个里面,没有哪个合适的,勉强选择的话只有mistral可以考虑,但这家伙中立的过分了,其实作为专业模型,我们不需要大模型给出态度和立场。

所以,行业模型还要从更基础的模型来做。

用Caddy部署wordpress

年前把公司网站的服务器换到了caddy上,今天想写一篇博客,发现证书过期了,之前用了certbot但是配置自动任务还是比较麻烦的,尤其是用了docker。索性一起换掉。

docker-compose.yml中的配置如下:

  caddy:
    image: registry.cn-beijing.aliyuncs.com/futuremeng/caddy:1.1
    container_name: caddy
    restart: unless-stopped
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - ./services/caddy/config:/config
      - ${DATA_DIR}/caddy:/data
      - ${SOURCE_DIR}:/www/:rw
      - ${LOGS_DIR}/caddy:/var/log/caddy
    ports:
      - "80:80"
      - "443:443"
    networks:
      - default

其中的env变量请自行脑补。

caddyfile:

https://WordPress.com {
        root * /www/WordPress
        php_fastcgi php80:9000 {
                trusted_proxies private_ranges
        }
        file_server
        encode gzip
        @disallowed {
                path /xmlrpc.php
                path *.sql
                path /wp-content/uploads/*.php
        }
        rewrite @disallowed '/index.php'
}

详情可见:

https://github.com/futuremeng/dnmp