轻量级开源 docker私服管理工具registry-admin【进阶】 k8s部署

前情提要:轻量级开源 docker私服管理工具部署实践
之前有做过registry-admin用docker-compose进行部署的实践,在此基础之上,今日进行该项目在k8s上部署的实践,以便于有需要的同学门参考。

至于如何搭建k8s集群,这部分网上有很详尽的说明,我在这里就不再写了,有需要在单开一章

我编写了部署文件,通过kubectl apply -f ${部署文件}.yaml来进行部署即可,所以接下来主要介绍一下部署文件的各部分内容。

命名空间

定义一个registry-admin资源归属的命名空间,没啥特别可说的

#
# registry-admin 部署文件
# 查看全部资源
# kubectl get svc,deploy,pod,configmaps,pv,pvc -n registry-admin
# 部署后测试
# registry-admin container curl http://$podip:5000/v2/_catalog
# registry container curl http://$podip:5080
# registry-admin svc http://$registry-admin-svcip
# registry svc curl http://$registry-svcip
#
apiVersion: v1
kind: Namespace  #命名空间
metadata:
  name: registry-admin
  
---

定义存储PVC

这里我使用的事本地存储,storageClass用的是racher版的local-path-provisioner实现
官方地址
相比于静态的hostPath或local volume优势是可以支持“动态申请”,“自动回收”
具体安装较为简单,网上有不少详细说明,我就不赘述了,有需要的话再补充,特别说明的是:
rancher的local-path-provisioner 支持2种回收策略
reclaimPolicy: Delete(容器销毁自动删除存储)默认
reclaimPolicy: Retain (容器销毁不自动删除,手动处理)
其本身也是k8s部署的,修改部署yaml进行容器发布即可,建议可以安装2个PV使用不同的回收策略,以便于在不同的应用场景中使用。我就安装了2个PV
storageClass: local-path-retain (保留)
storageClass:local-path (自动清除)
此处我使用的是默认策略的local-path

#声明存储使用
#local存储  storage_class:local-path-provisioner(rancher)
apiVersion: v1 
kind: PersistentVolumeClaim
metadata:
  namespace: registry-admin
  name: local-path-pvc
spec:
  accessModes:
    - ReadWriteOnce #本地存储只支持ReadWriteOnce
  storageClassName: local-path #local-path: 容器删除,存储local-path动态删除,local-path-retain: 容器删除,存储local-path保留,local-path|local-path-retain为手动安装的storageclass
  resources:
    requests:
      storage: 10Gi #声明最少要使用存储空间,不足则无法创建 Gi=G Mi=M 
  #persistentVolumeReclaimPolicy: Delete # PVC 回收策略 Retain 保留| Delete 清除 | PV: local-path-provisioner(rancher) 不支持设置该属性

---

定义service

我们用到2个容器
regisrty-admin:管理界面应用
registry:私服应用
为了能够通过服务名称访问而不是绑定ip,因此我们也需要对应创建2个service,配置service的name分别为:regisrty-admin,registry,这样在k8s集群内部就可以通过http://regisrty-admin与http://registry访问,而我们实际使用时,是从k8s集群之外的节点发起请求(比如自己用的笔记本电脑),因此还需要给2个service 暴露nodePort 以便通过k8s-nodeip:nodeport访问应用界面以及进行私服仓库的pull与push。
如下配置发布成功后,我们便可以通过
http://k8s-node:30580 访问管理界面
http://k8s-node:30500 访问私服的api

kind: Service
apiVersion: v1
metadata:
  #自定义标签属性列表
  labels:
    k8s-app: registry-admin
  name: registry-admin
  namespace: registry-admin
  #自定义注解属性列表  
  annotations: 
    desc : registry-admin http服务访问入口
  
spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: 5080
      nodePort: 30580
  selector:
    # abel selector配置,将选择具有label标签的Pod作为管理 
    k8s-app: registry-admin  
status:
  loadBalancer: {}

#当spce.type=LoadBalancer时,设置外部负载均衡器的地址
#status:                               
#    loadBalancer:                        #外部负载均衡器    
#      ingress:                           #外部负载均衡器 
#        ip: string                       #外部负载均衡器的Ip地址值
#        hostname: string                 #外部负载均衡器的主机名

---

kind: Service
apiVersion: v1
metadata:
  #自定义标签属性列表
  labels:
    k8s-app: registry-admin
  name: registry
  namespace: registry-admin
  #自定义注解属性列表  
  annotations: 
    desc : registry-admin http服务访问入口
  
spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: 5000
      nodePort: 30500
  selector:
    # abel selector配置,将选择具有label标签的Pod作为管理 
    k8s-app: registry-admin  
status:
  loadBalancer: {}
  
    
---

定义配置

还是2个配置,1个是registry-admin的,1个是registry的,进行basic验证的文件.htpasswd不需要提前创建了,在使用中发现registry-admin会自动创建,只需要通过容器的volumeMounts让2个容器共享这个部分存储即可(在接下来的deployment配置中会有)
特别说明: ${ base64 encode string } 这个值是个base64的加密串,其原始值为admin:{basic-ra-config.yml中定义的registry.password}

apiVersion: v1
kind: ConfigMap #配置信息
metadata:
  name: registry-admin-config
  namespace: registry-admin
data:
  basic-ra-config.yml: |
    hostname: 127.0.0.1
    port: 5080
    registry:
      host: http://registry
      port: 80
      auth_type: basic
      htpasswd: /access/.htpasswd
      login: admin
      password: $password
      gc_interval: 20
    store:
      type: embed
      admin_password: $password
      embed:
      path: /app/data/store.db
    logger:
      enabled: true
      filename: /app/log/access.log # mount the directory to a docker host folder for get access for fail2ban
      max_size: 10M
      max_backups: 3
---


apiVersion: v1
kind: ConfigMap #配置信息
metadata:
  name: registry-config
  namespace: registry-admin
data:
  config.yml: |
    version: 0.1
    log:
      accesslog:
        disabled: false
      level: info
      formatter: text
      fields:
        service: registry
    storage:
      filesystem:
        rootdirectory: /registry/lib
        maxthreads: 100
      delete:
        enabled: true
    http:
      addr: ":5000"
      net: tcp
    auth:
      htpasswd:
        realm: basic-realm
        path: /access/.htpasswd
    notifications:
      events:
        includereferences: true
      endpoints:
        - name: ra-listener
          disabled: false
          url: http://registry-admin/api/v1/registry/events
          headers:
            Authorization: [ Basic ${base64 encode string} ] # 'admin:$password' base64 encode string
          timeout: 1s
          threshold: 5
          backoff: 3s
          ignoredmediatypes:
            - application/octet-stream
          ignore:
            mediatypes:
              - application/octet-stream
---

StatefulSet定义

以下为核心2个StatefulSet的定义,其中要特别说明的有:

  • 2个容器同时挂载了/access目录,并指向同个物理存储目录,以便于共享.htpasswd文件,这种处理方式也是常用的共享文件使用方式,主要是由registry-admin控制账号,而2者启动后都会自动往.htpasswd中初始化用户,因此一定要按照先registry,再registry-admin的顺序启动容器。
- name: registry-admin-volume #挂载共享文件,同个pod内容器可以同时读写
  mountPath: /access
  subPath: ./access
  • registry-admin 作者并未实现分布式session,因此应用只能单点跑,replicas=1

  • registry-admin-app 设置了环境变量,除了指定应用文件的位置外(RA_CONFIG_FILE),还设定了APP_UID 让其使用root用户启动(经尝试配置privileged: true也不行,必须通过应用环境变量设定),最后添加 POD_NAME环境变量,如果启动多个副本(这里我只部署了1个),可以把日志写入到各自目录中,这也是同个deployment运行多个副本时,避免写串日志的常用方式。

env:  #环境变量配置
  - name: RA_CONFIG_FILE   # key-value 配置
    value: /app/config/basic-ra-config.yml
  - name: APP_UID #指定使用root用户启动registry-admin 否则无法将Rest bind 80端口
    value: "0"
  - name: POD_NAME
    valueFrom:
      fieldRef:
        apiVersion: v1
          fieldPath: metadata.name

在上面配置了POD_NAME的基础之上,通过subPathExpr,动态指定日志目录的物理存放位置

 - name: registry-admin-volume
   mountPath: /app/log
   #subPath: ./registry-admin/log
   subPathExpr: log/$(POD_NAME)/   #使用$(POD_NAME) 挂载动态目录
  • k8s 容器默认会使用utc时间,为了让其使用本地时区的时间设置,需要挂载本地时区的配置文件
- name: host-time  #挂载本地时区
  mountPath: /etc/localtime
  readOnly: true
name: host-time
hostPath: #指向到当前node本地时区配置
  path: /etc/localtime
  type: ""
  • 添加registry垃圾回收定时任务并启动定时调度
    在实际使用中发现,通过registry-admin删除已上传的镜像后,registry中的仓库存储空间没有变化,一开始以为是registry-admin的bug,经过查看源代码发现并不是,实际registry-admin通过调用registry的delete api完成的删除,是没问题的。然后查阅docker registry官网发现,按照官方设计,再删除后比手动指定垃圾回收才会删除物理文件(让我有点无语),即执行:
    registry garbage-collect /etc/docker/registry/config.yml
    为此我在registry容器启动后,通过lifecycle的postStart钩子函数,创建垃圾回收的定时任务,日志清除的定期任务,并启动crond调度服务(对,没错,官方的镜像实际连系统调度服务都不会自动起!!!),以下为配置片段。

值得注意的是即便执行了垃圾回收,通过 http://registry/v2/_catalog 查看,仍然能看到被删除的镜像目录(目录是空的),好像必须手动删除才行(我感觉官方压根就不想让你好好用免费的私服,想让你买收费服务。。。。)

lifecycle:
  postStart:  #通过postStart 钩子函数做初始化|如果失败会重启容器
    exec: 
      command: #在钩子函数中command也支持多行命令只是不支持args
        - "sh" 
        - "-c"
        - >  # 设置registry垃圾回收器的定时任务,清理日志的定时任务并启动调度服务
          echo '#!/bin/sh' > /etc/periodic/15min/registry-gc;
          echo 'echo $(date +%Y%m%d_%H%M%S) registry" gc start " >> /var/log/registry-gc.log' >> /etc/periodic/15min/registry-gc;
          echo 'registry garbage-collect /etc/docker/registry/config.yml' >> /etc/periodic/15min/registry-gc;
          echo 'echo $(date +%Y%m%d_%H%M%S) registry" gc executed " >> /var/log/registry-gc.log' >> /etc/periodic/15min/registry-gc;
          echo '#!/bin/sh' > /etc/periodic/weekly/clean-log;
          echo 'cat /dev/null > /var/log/registry-gc.log' >> /etc/periodic/weekly/clean-log;
          echo 'cat /dev/null > /var/log/cron.log' >> /etc/periodic/weekly/clean-log;
          chmod a+x /etc/periodic/15min/registry-gc;
          chmod a+x /etc/periodic/weekly/clean-log;
          crond -L /var/log/cron.log             

以下为deployment部署的完整片段

apiVersion: apps/v1
kind: StatefulSet # Deployment | StatefulSet | DaemonSet | JobSet
metadata: 
  name: registry-admin-sts
  namespace: registry-admin
spec: 
  replicas: 1  #运行副本数
  selector: 
    matchLabels: 
       k8s-app: registry-admin #与下方template节点中的 labels 保持一致
  revisionHistoryLimit: 10 #设定保留最近的几个revision 用于回滚,默认10
  #serviceName: "nginx-headless" #设置绑定的service,以支持内部dns访问 <pod-name>.<svc-name>.<namespace>.svc.cluster.local
  updateStrategy: #更新策略
    type: RollingUpdate # RollingUpdate (滚动更新) | OnDelete (删除时更新)
    rollingUpdate:
      #maxSurge: 1  #[Deployment]支持-升级过程中可以启动超过原先设置的POD数量的上限:数量 或 百分比 1 | 20%
      #maxUnavailable: 1 #[Deployment]支持-升级过程中无法提供服务的POD数量的上限:数量 或 百分比 1 | 20%,最好与maxSurge保持一致,这样能确保更新过程中的服务能力不会下降
      partition: 0 #灰度发布控制器,每次只更新部署的pod序号 >= partition的pod,如果有5个pod[0-4],0=更新所有,4=更新1pod,3=更新2pod
  template: 
    metadata: 
      labels: 
         k8s-app: registry-admin
    spec: 
      restartPolicy: Always     #默认即为 Always | OnFailure | Never
      terminationGracePeriodSeconds: 30 #容器被删除变为Terminating状态的等待时间,默认是30s,以便于做一些容器删除前的处理工作
      containers: 
        - name: registry-app
          image: registry:2.8.3
          imagePullPolicy: IfNotPresent
          securityContext: ###添加参数启用容器root权限
            privileged: true
          ports: 
            - containerPort: 5000
              protocol: TCP
          #requests: 
            #cpu: "300m"    # 容器所需cpu资源 | 可以超过 单位milicpu,500mcpu=0.5cpu
            #memory: "64Mi" # 容器所需内存资源 | 可以超过;但如果超过,容器可能会在Node内存不足时清理 单位则包括E, P, T, G, M, K, Ei, Pi, Ti, Gi, Mi, Ki等
          #limits:
            #cpu: "500m"     # 容器cpu上限 | 可以短暂超过,容器也不会被停止
            #memory: "128Mi" # 容器内存上限 | 不可以超过;如果超过,容器可能会被停止或调度到其他资源充足的机器上
          #command: ["/bin/sh","-c"] #添加registry垃圾回收定时任务,并启动系统定时调度服务
          #args: #可以设置多行命令,不过启动后初始化还是推荐使用postStart钩子函数来执行
            #- | 
              #registry垃圾回收器的定时任务
              #echo '#!/bin/sh' > /etc/periodic/15min/registry-gc
              #echo 'echo $(date +%Y%m%d_%H%M%S) registry" gc start " >> /var/log/registry-gc.log' >> /etc/periodic/15min/registry-gc
              #echo 'registry garbage-collect /etc/docker/registry/config.yml' >> /etc/periodic/15min/registry-gc
              #echo 'echo $(date +%Y%m%d_%H%M%S) registry" gc executed " >> /var/log/registry-gc.log' >> /etc/periodic/15min/registry-gc
              #清理日志的定时任务
              #echo '#!/bin/sh' > /etc/periodic/weekly/clean-log
              #echo 'cat /dev/null > /var/log/registry-gc.log' >> /etc/periodic/weekly/clean-log
              #echo 'cat /dev/null > /var/log/cron.log' >> /etc/periodic/weekly/clean-log
              #chmod a+x /etc/periodic/15min/registry-gc
              #chmod a+x /etc/periodic/weekly/clean-log
              #crond -L /var/log/cron.log
              #registry serve /etc/docker/registry/config.yml
          lifecycle:
            postStart:  #通过postStart 钩子函数做初始化|如果失败会重启容器
              exec: 
                command: #在钩子函数中command也支持多行命令只是不支持args
                  - "sh" 
                  - "-c"
                  - >  # 设置registry垃圾回收器的定时任务,清理日志的定时任务并启动调度服务
                    echo '#!/bin/sh' > /etc/periodic/15min/registry-gc;
                    echo 'echo $(date +%Y%m%d_%H%M%S) registry" gc start " >> /var/log/registry-gc.log' >> /etc/periodic/15min/registry-gc;
                    echo 'registry garbage-collect /etc/docker/registry/config.yml' >> /etc/periodic/15min/registry-gc;
                    echo 'echo $(date +%Y%m%d_%H%M%S) registry" gc executed " >> /var/log/registry-gc.log' >> /etc/periodic/15min/registry-gc;
                    echo '#!/bin/sh' > /etc/periodic/weekly/clean-log;
                    echo 'cat /dev/null > /var/log/registry-gc.log' >> /etc/periodic/weekly/clean-log;
                    echo 'cat /dev/null > /var/log/cron.log' >> /etc/periodic/weekly/clean-log;
                    chmod a+x /etc/periodic/15min/registry-gc;
                    chmod a+x /etc/periodic/weekly/clean-log;
                    crond -L /var/log/cron.log;
                                        
          volumeMounts: 
            - name: registry-config
              mountPath: /etc/docker/registry/ 
               #subPath: /registry-admin/conf #通过subpath指定子目录控制数据区分与共享
            - name: registry-admin-volume
              mountPath: /certs
              subPath: ./certs
            - name: registry-admin-volume  #挂载共享文件,同个pod内容器可以同时读写
              mountPath: /access
              subPath: ./access
            - name: registry-admin-volume
              mountPath: /registry/lib
              subPath: ./registry/lib/
            - name: host-time  #挂载本地时区
              mountPath: /etc/localtime
              readOnly: true
         #env:  #环境变量配置
            #- name: RA_CONFIG_FILE   # key-value 配置
            #  value: /app/config/basic-ra-config.yml
              #valueFrom:
                #fieldRef:
                  #fieldPath: metadata.namespace # 以当前行容器运行参数信息为数据来源
        - name: registry-admin-app
          image: zebox/registry-admin:latest
          #image: registry:80/registry-admin:latest #使用私服
          #imagePullSecrets:                        #私服认证信息
            #- name: myregistrykey                  #私服账号secret资源名称,需要单独创建:kubectl create secret generic... 详见:https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/pull-image-private-registry/
          imagePullPolicy: IfNotPresent # IfNotPresent | Always | Never
          securityContext: ###添加参数启用容器root权限
            privileged: true
          ports: 
            - containerPort: 5080
              protocol: TCP
          #requests: 
            #cpu: "300m"    # 容器所需cpu资源 | 可以超过 单位milicpu,100mcpu=0.1cpu(0.1核)
            #memory: "64Mi" # 容器所需内存资源 | 可以超过;但如果超过,容器可能会在Node内存不足时清理 单位则包括E, P, T, G, M, K, Ei, Pi, Ti, Gi, Mi, Ki等
          #limits:
            #cpu: "500m"     # 容器cpu上限 | 可以短暂超过,容器也不会被停止
            #memory: "128Mi" # 容器内存上限 | 不可以超过;如果超过,容器可能会被停止或调度到其他资源充足的机器上
          #command:
            #- /bin/sh
            #- -c
            #- chown -R 1001:1001 /app
          #startupProbe: #容器启动探针,失败自动重启 结果Success:表示通过检测| Failure:表示未通过检测|Unknown:表示检测没有正常进行。 
            #exec: #支持3种方式 httpGet http url | tcpSocket tcp 端口建连 | exec 执行命令
            #  command: 
            #    - cat                   
            #    - /tmp/healthy
            #initialDelaySeconds: 0 #容器启动后要等待多少秒后就探针开始工作,单位“秒”,默认是 0 秒,最小值是 0
            #periodSeconds: 10 #执行探测的时间间隔(单位是秒),默认为 10s,单位“秒”,最小值是 1
            #timeoutSeconds: 2 #探针执行检测请求后,等待响应的超时时间,默认为 1s,单位“秒”,最小值是 1
            #successThreshold: 1 #探针检测失败后认为成功的最小连接成功次数,默认为 1s,在 Liveness 探针中必须为 1,最小值为 1。
            #failureThreshold: 3 #探测失败的重试次数,重试一定次数后将认为失败,在 readiness 探针中,Pod会被标记为未就绪,默认为 3,最小值为 1
            
          #livenessProbe: #容器存活探针,失败自动重启
            #httpGet:
              #host: #要连接的主机名,默认为Pod IP,可以在http request head中设置host头部。
              #port: 80 #容器上要访问端口号或名称. *
              #path: /health #http服务器上的访问URI。*
              #scheme: http #用于连接host的协议,默认为HTTP。
              #httpHeaders: #自定义HTTP请求headers,HTTP允许重复headers。    
                #- name: Custom-Header
                #  value: Awesome              
            #initialDelaySeconds: 30
            #periodSeconds: 10
            #timeoutSeconds: 2
            #successThreshold: 1
            #failureThreshold: 3
            
          #readinessProbe: #容器就绪探针,失败不容器,容器不会ready,一般有startup与liveness就够了
            #tcpSocket:
              #port: 8080
            #initialDelaySeconds: 10 
            #periodSeconds: 10
            #timeoutSeconds: 2
            #successThreshold: 1
            #failureThreshold: 3
          volumeMounts: 
            - name: registry-admin-config
              mountPath: /app/config #通过configMap 挂载配置文件(只读)
              #subPath: /registry-admin/conf #通过subpath指定子目录控制数据区分与共享
            - name: registry-admin-volume
              mountPath: /certs
              subPath: ./certs
            - name: registry-admin-volume #挂载共享文件,同个pod内容器可以同时读写
              mountPath: /access
              subPath: ./access
            - name: registry-admin-volume
              mountPath: /app/data
              subPath: ./registry-admin/data
            - name: registry-admin-volume
              mountPath: /app/log
              #subPath: ./registry-admin/log
              subPathExpr: log/$(POD_NAME)/   #使用$(POD_NAME) 挂载动态目录
            - name: host-time  #挂载本地时区
              mountPath: /etc/localtime
              readOnly: true
          env:  #环境变量配置
            - name: RA_CONFIG_FILE   # key-value 配置
              value: /app/config/basic-ra-config.yml
            - name: APP_UID #指定使用root用户启动registry-admin 否则无法将Rest bind 80端口
              value: "0"
            #- name: RA_REGISTRY_GC_INTERVAL
            #  value: "15"
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.name
              #valueFrom:
                #fieldRef:
                  #fieldPath: metadata.namespace # 以当前行容器运行参数信息为数据来源
      volumes: 
        - name: registry-admin-config
          configMap:    #使用configMap
            name: registry-admin-config
        - name: registry-config
          configMap:    #使用configMap
            name: registry-config
        - name: registry-admin-volume  #使用pvc
          persistentVolumeClaim:
            claimName: local-path-pvc
        - name: host-time
          hostPath: #挂载本地时区
            path: /etc/localtime
            type: ""
          


---

部署

部署应用

kubectl apply -f ${部署描述文件).yaml

卸载应用 (按默认配置会自动回收存储空间)

kubectl delete -f ${部署描述文件).yaml

查看部署情况

kubectl get deploy,pod,svc,pv,pvc,configmap -n registry-admin(命名空间) -o wide

进入容器

kubectl exec ${pod-name} -it -n registry-admin(命名空间) -c $容器名称(registry-app或者registry-admin-app) sh

部署成功后可以通过
http://k8s-node:30580 访问管理界面
http://k8s-node:30500 访问私服的api
我额外做了tengine配置,用80做了方向代理,建议大家也配置一下,后续使用更方便。

完整部署文件我已通过资源绑定上传,通过文件顶部下载即可。

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
以下是搭建私服镜像中心docker-registrydocker-registry-web的步骤: 1.安装DockerDocker Compose 2.创建一个目录来存储docker-compose.yml文件和证书文件 3.创建docker-compose.yml文件并添加以下内容: ```yaml version: '3' services: registry: restart: always image: registry:2 ports: - 5000:5000 environment: REGISTRY_AUTH: htpasswd REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm REGISTRY_STORAGE_DELETE_ENABLED: "true" REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt REGISTRY_HTTP_TLS_KEY: /certs/domain.key volumes: - ./data:/var/lib/registry - ./auth:/auth - ./certs:/certs registry-web: restart: always image: mkuchin/docker-registry-web:v0.1.2 ports: - 8080:8080 environment: REGISTRY_URL: https://registry:5000 REGISTRY_WEB_TITLE: Docker Registry REGISTRY_AUTH: htpasswd REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt REGISTRY_HTTP_TLS_KEY: /certs/domain.key volumes: - ./auth:/auth - ./certs:/certs ``` 4.创建一个目录来存储证书文件和htpasswd文件 5.生成证书文件 ```shell openssl req -newkey rsa:4096 -nodes -sha256 -keyout domain.key -x509 -days 365 -out domain.crt ``` 6.生成htpasswd文件 ```shell htpasswd -Bc auth/htpasswd <username> ``` 7.启动docker-compose ```shell docker-compose up -d ``` 8.访问https://<your-domain>:8080,输入用户名和密码即可登录docker-registry-web界面。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

windywolf301

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值