Ubuntu镜像(纯粹版本)

之前我在 Ubuntu镜像(采用tini替代systemd) 实践中,一直努力在构建一个能够模拟全功能虚拟机的镜像,也就是在一个容器中同时运行多个服务,特别是 ssh服务 以及 Nginx 这样的基础服务,以便构建一个灵活的开发环境。

不过,有得必有失,过于复杂的容器包装,导致了容器失去了灵活性以及符合 Kubernetes 运行标准的能力,或者说兼容更为繁琐臃肿。所以,大道至简,我现在更倾向于采用标准且轻量的容器,通过编排来灵活组合。

沿用 podman images 实践经验,我现在调整构建 Ubuntu 镜像,来为 容器直接访问AMD GPU 提供基础

base镜像

首先创建一个基础镜像

  • 创建 Dockerfile 为Ubuntu构建基本的系统升级和用户帐号环境:

基础镜像Dockerfile
# Use latest Ubuntu
FROM ubuntu:latest

RUN apt update -y && apt upgrade -y

# Devops utilities
RUN apt -y install sudo openssh-client bind9-dnsutils tmux git vim

# create admin (UID/GID 1000)
ARG USER=admin
ARG GROUP=admin
ARG UID=1000
ARG GID=1000

# Ubuntu images has "ubuntu" account: uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)
# I change "ubuntu" to "admin" ,maybe you don't need this step
# Ubuntu images has "video" group: gid=44(video)
# I add "render" group: gid=993(render) , for AMD GPU
# and I add admin to group: sudo(27), adm(4), video(44), render(44), admin(1000)

RUN groupmod -n $USER ubuntu
RUN usermod -l $USER -d /home/$USER -m ubuntu

RUN groupadd -g 993 render && \
    usermod  -aG render $USER 

RUN sed -i 's/%sudo	ALL=(ALL:ALL) ALL/%sudo ALL=(ALL:ALL) NOPASSWD: ALL/g' /etc/sudoers

USER $USER
COPY vimrc /home/$USER/.vimrc 

USER root
CMD ["/bin/bash"]

备注

  • Ubuntu官方镜像中默认设置了 ubuntu 帐号(uid=1000,gid=1000),我修订为 admin 以便和我的集群中Host主机对齐

  • 通过对比镜像中组名和组id,我增加了一个 render 用于后续AMD GPU所使用 ROCm 所用组对齐

  • 调整 /etc/sudoers 时需要主机默认配置中列分割符号是 TAB 而不是空格,所以在执行 sed 修改时务必复制粘贴正确,否则替换会失败

  • 执行镜像构建:

执行镜像构建
docker build --rm -t ubuntu-base .
  • 运行容器:

运行容器
docker run -d \
  --name ubuntu-base \
  --hostname ubuntu-base \
  -p 8080:8080 \
  -p 8443:8443 \
  -p 9000:9000 \
  --user 1000:1000 \
  -e LANG=C.UTF-8 \
  --workdir /workspace \
  -v /home/admin/docs:/workspace \
  -v /home/admin/.ssh:/home/admin/.ssh \
  ubuntu-base \
  sh -c "sleep infinity"

备注

docker run 时使用了参数 --user 1000:1000 : 这个参数可以让容器始终运行在指定 uid/gid 下,对安全有很大提高。

不过,这个参数也带来一个问题: 当使用了 --user 1000:1000 参数之后,Docker默认只加载该UID的主组:

  • docker exec -it ubuntu-base /bin/bash 进入容器,可以看到进入时身份就是 uid=1000 的用户 admin ,并且执行 id 命令可以看到该用户只有一个主组,其他在 /etc/group 中设置的 gid 都不生效:

id 命令输出显示 admin 用户只有主组生效
admin@ubuntu-base:~$ id
uid=1000(admin) gid=1000(admin) groups=1000(admin)
  • 正因为 admin 只有主组生效,所以即使镜像中已经修订了 /etc/sudoers ,但是 adminsudo 组没有生效,无法使用 sudo 命令

dev镜像

开发镜像中增加了开发工具并设置初步环境
# Use latest Ubuntu
FROM ubuntu:latest

RUN apt update -y && apt upgrade -y

# Devops utilities
RUN apt -y install sudo openssh-client bind9-dnsutils tmux git vim

# create admin (UID/GID 1000)
ARG USER=admin
ARG GROUP=admin
ARG UID=1000
ARG GID=1000

# Ubuntu images has "ubuntu" account: uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)
# I change "ubuntu" to "admin" ,maybe you don't need this step
# Ubuntu images has "video" group: gid=44(video)
# I add "render" group: gid=993(render) , for AMD GPU
# and I add admin to group: sudo(27), adm(4), video(44), render(44), admin(1000)

RUN groupmod -n $USER ubuntu
RUN usermod -l $USER -d /home/$USER -m ubuntu

RUN groupadd -g 993 render && \
    usermod  -aG render $USER 

RUN sed -i 's/%sudo	ALL=(ALL:ALL) ALL/%sudo ALL=(ALL:ALL) NOPASSWD: ALL/g' /etc/sudoers

# vim simple config
COPY vimrc /home/$USER/.vimrc 
RUN chown $USER:$GROUP /home/$USER/.vimrc

# helix
COPY maveonair-ubuntu-helix-editor-noble.sources /etc/apt/sources.list.d/
RUN apt update && apt install helix -y

# Go
RUN apt-get install -y golang-go

USER $USER
WORKDIR /home/$USER

# install go tools to $HOME/go/bin
RUN go install golang.org/x/tools/gopls@latest && \
    go install github.com/nametake/golangci-lint-langserver@latest && \
    go install github.com/go-delve/delve/cmd/dlv@latest
# if in CHINA, please use:
#     RUN GOPROXY=https://goproxy.cn,direct go install ...
    
# set PATH include $HOME/go/bin
ENV PATH="${HOME}/go/bin:${PATH}"

# install llm-ls
RUN curl -L https://github.com/huggingface/llm-ls/releases/download/0.5.3/llm-ls-x86_64-unknown-linux-gnu.gz | gzip -d -c > /usr/local/bin/llm-ls && chomd +x /usr/local/bin/llm-ls

CMD ["/bin/bash"]

为了能够解决使用 --user 1000 参数执行 docker run 只加载主组的问题,需要配合 --group-add 参数来为 admin 增加辅助组,以便能够使用 sudo 以及AMD GPU。这种方式在开发环境会非常方便,能够从 admin 用户随时切换到 root 进行一些系统操作:`

运行容器,为admin用户添加组
docker run -d \
  --name ubuntu-base \
  --hostname ubuntu-base \
  -p 8080:8080 \
  -p 8443:8443 \
  -p 9000:9000 \
  --user 1000:1000 \
  # 开发环境允许adm,sudo组用于管理,允许video,render组用于AMD GPU开发
  --group-add 4 \
  --group-add 27 \
  --group-add 44 \
  --group-add 993 \
  -e LANG=C.UTF-8 \
  --workdir /workspace \
  -v /home/admin/docs:/workspace \
  -v /home/admin/.ssh:/home/admin/.ssh \
  # 开发环境允许调试和跟踪
  --cap-add SYS_PTRACE \
  --security-opt seccomp=unconfined \
  ubuntu-base \
  sh -c "sleep infinity"

这里 --group-add 分别为 admin 用户添加了组:

  • 4(adm) : 在 Ubuntu Linux 中, adm 组用于 查看系统日志(Monitoring & Auditing) ,而无需获得 root 权限。该组成员对 /var/log/ 目录下大部分日志(如 syslog , auth.log , kern.log )拥有读取权限,这样 admin 能够在调试程序时方便查看系统日志

  • 27(sudo) : 该组成员通过 sudo 提升为 root 用户拥有整个系统超级权限,适合在特定情况下处理系统维护工作

  • 44(video)993(render) 在开发和维护 AMD GPU 时需要该组身份权限

开发环境的系统能力和安全策略

在dev镜像运行时,特别使用了:

  • --cap-add SYS_PTRACE : 允许容器内部的进程调用 ptraceptrace 是Linux下所有调试工具的核心。例如在容器中使用 strace系统调用跟踪 查看Go程序的系统调用,或者用 gdb 调试崩溃堆栈,都需要这个权限

  • --security-opt seccomp=unconfined : 将 seccomp 设置为 unconfined (不限制)表示允许容器内的进程能够尝试调用任何内核支持的指令。这对于开发环境、内核调试或性能工具(如 Linux perf性能分析工具 )是必不可少的。如果不设置这个参数,那么即使允许了上文的 SYS_PTRACE 能力,则依然可能拦截 ptrace 的某些子功能。(Docker默认会禁止约40多个危险或不常用的系统调用)

警告

在生产环境运行容器绝对不要使用上述两个开发环境的运行参数,存在重大安全隐患

在开发环境中,如果Ubuntu容器中运行 llm-ls 没有反应,可以尝试配置:

  • --sysctl net.core.somaxconn=1204 高并发环境测试监控脚本

  • --shm-size=2g 如果涉及到GPU显存和大量内存操作,默认容器的 /dev/shm 只有 64MB 可能会导致深度学习框架或高性能内存映射我呢键(mmap)崩溃

ENV

上述 dev 镜像中使用了 ENV 命令:

Dockerfile中 ENV 指令`
# set PATH include $HOME/go/bin
ENV PATH="${HOME}/go/bin:${PATH}"

在Dockerfile中使用 ENV 指令,该配置行 不会添加 到容器内的任何"文件" (如 .bashrc/etc/profile )中,而是直接写入镜像的 元数据(Metadata) 里:

  • 在镜像的JSON配置文件中(通过 docker inspect <image_id> 查看)

  • 当容器启动时,Docker守护进程( containerd运行时(runtime) )会读取这些元数据,并在创建容器进程(Runtime)时,直接通过系统调用将这些变量注入到进程的环境变量列表( envp )中

ENV.bashrc 区别

特性

Dockerfile ENV

.bashrc / /etc/environment

存储方式

镜像元数据 (Static Metadata)

磁盘上的文本文件 (File on Disk)

加载时机

PID 1 进程启动前

Shell 启动时 (Interactive login)

覆盖范围

所有进程(即便不运行 Shell)

仅限通过 Shell 启动的进程

可见性

docker inspect 可见

只有进入容器后 cat 文件可见

上述使用 ENV 设置镜像环境变量在执行类似 docker exec <container> gopls 命令时,由于没有使用SHELL,会导致 .bashrc 中设置的环境变量失效,但是如果在 ENV 中定义的环境变量则依然有效。