D5.3 Private Registry ฮผฮต Ansible ๐Ÿ›๏ธ

ฮคฮน ฮธฮฑ ฯ†ฯ„ฮนฮฌฮพฮฟฯ…ฮผฮต

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Private Docker Registry                                โ”‚
โ”‚                                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  nextcloud (registry server)                     โ”‚   โ”‚
โ”‚  โ”‚                                                  โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚   registry:2       โ”‚  โ”‚   nginx (SSL/Auth) โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚   :5000 (internal) โ”‚  โ”‚   :443 (external)  โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚   โ”‚
โ”‚  โ”‚                                                  โ”‚   โ”‚
โ”‚  โ”‚  /opt/registry/                                  โ”‚   โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ data/          โ† image storage              โ”‚   โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ auth/          โ† htpasswd                   โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€ certs/         โ† SSL certificates           โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                         โ”‚
โ”‚  Clients:                                               โ”‚
โ”‚  docker login registry.example.com                      โ”‚
โ”‚  docker push registry.example.com/myapp:1.0.0           โ”‚
โ”‚  docker pull registry.example.com/myapp:1.0.0           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ฮ”ฮฟฮผฮฎ Role

# ฮ”ฮทฮผฮนฮฟฯ…ฯฮณฮฏฮฑ registry role
cd ~/ansible/roles
ansible-galaxy role init registry

tree ~/ansible/roles/registry/
# registry/
# โ”œโ”€โ”€ defaults/
# โ”‚   โ””โ”€โ”€ main.yml
# โ”œโ”€โ”€ handlers/
# โ”‚   โ””โ”€โ”€ main.yml
# โ”œโ”€โ”€ tasks/
# โ”‚   โ”œโ”€โ”€ main.yml
# โ”‚   โ”œโ”€โ”€ setup.yml
# โ”‚   โ”œโ”€โ”€ auth.yml
# โ”‚   โ”œโ”€โ”€ deploy.yml
# โ”‚   โ””โ”€โ”€ verify.yml
# โ””โ”€โ”€ templates/
#     โ”œโ”€โ”€ registry-compose.yml.j2
#     โ””โ”€โ”€ nginx-registry.conf.j2

defaults/main.yml

cat > ~/ansible/roles/registry/defaults/main.yml << 'EOF'
---
# ============================================================
# Private Registry Defaults
# ============================================================

# โ”€โ”€ Registry Settings โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
registry_name:     registry
registry_port:     5000              # internal port
registry_ext_port: 443              # external port (SSL)
registry_hostname: "{{ inventory_hostname }}"

# โ”€โ”€ Storage โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
registry_base_dir:   /opt/registry
registry_data_dir:   /opt/registry/data
registry_auth_dir:   /opt/registry/auth
registry_certs_dir:  /opt/registry/certs
registry_logs_dir:   /opt/registry/logs

# โ”€โ”€ Authentication โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
registry_auth_enabled: true
registry_users:        []
# Format:
# - username: myuser
#   password: "{{ vault_registry_pass }}"

# โ”€โ”€ SSL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
registry_ssl_enabled:    true
registry_ssl_self_signed: true      # โ† true ฮณฮนฮฑ self-signed
registry_ssl_cert_days:  3650       # 10 ฯ‡ฯฯŒฮฝฮนฮฑ

# โ”€โ”€ Images โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
registry_image:   registry:2
nginx_image:      nginx:1.25.3-alpine

# โ”€โ”€ Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
registry_memory:  "256m"
registry_cpus:    "0.25"
nginx_memory:     "64m"
nginx_cpus:       "0.25"

# โ”€โ”€ Garbage Collection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
registry_gc_enabled:  true
registry_gc_schedule: "0 2 * * 0"  # ฮšฯ…ฯฮนฮฑฮบฮฎ 02:00
EOF

tasks/main.yml

cat > ~/ansible/roles/registry/tasks/main.yml << 'EOF'
---
# ============================================================
# Private Registry โ€” Main Entry Point
# ============================================================

- name: Setup registry infrastructure
  ansible.builtin.import_tasks:
    file: setup.yml
  tags: [setup, registry]

- name: Configure authentication
  ansible.builtin.import_tasks:
    file: auth.yml
  tags: [auth, registry]

- name: Deploy registry
  ansible.builtin.import_tasks:
    file: deploy.yml
  tags: [deploy, registry]

- name: Verify registry
  ansible.builtin.import_tasks:
    file: verify.yml
  tags: [verify, registry]
EOF

tasks/setup.yml

cat > ~/ansible/roles/registry/tasks/setup.yml << 'EOF'
---
# ============================================================
# Registry Setup โ€” Directories & Certificates
# ============================================================

# โ”€โ”€ Directories โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Create registry directories
  ansible.builtin.file:
    path:  "{{ item }}"
    state: directory
    mode:  '0755'
    owner: root
    group: root
  loop:
    - "{{ registry_base_dir }}"
    - "{{ registry_data_dir }}"
    - "{{ registry_auth_dir }}"
    - "{{ registry_certs_dir }}"
    - "{{ registry_logs_dir }}"

# โ”€โ”€ Self-signed SSL certificate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Generate self-signed SSL certificate
  ansible.builtin.command:
    cmd: >
      openssl req -newkey rsa:4096 -nodes -sha256
      -keyout {{ registry_certs_dir }}/registry.key
      -x509
      -days {{ registry_ssl_cert_days }}
      -out {{ registry_certs_dir }}/registry.crt
      -subj "/CN={{ registry_hostname }}"
      -addext "subjectAltName=DNS:{{ registry_hostname }},IP:{{ ansible_facts['default_ipv4']['address'] }}"
  args:
    creates: "{{ registry_certs_dir }}/registry.crt"
  when: registry_ssl_self_signed
  register: cert_created

- name: Set certificate permissions
  ansible.builtin.file:
    path: "{{ item.path }}"
    mode: "{{ item.mode }}"
  loop:
    - path: "{{ registry_certs_dir }}/registry.crt"
      mode: '0644'
    - path: "{{ registry_certs_dir }}/registry.key"
      mode: '0600'
  when: registry_ssl_self_signed

# โ”€โ”€ Trust certificate (ฮณฮนฮฑ Docker) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Create Docker certs directory
  ansible.builtin.file:
    path:  "/etc/docker/certs.d/{{ registry_hostname }}:{{ registry_ext_port }}"
    state: directory
    mode:  '0755'

- name: Copy certificate to Docker certs
  ansible.builtin.copy:
    src:        "{{ registry_certs_dir }}/registry.crt"
    dest:       "/etc/docker/certs.d/{{ registry_hostname }}:{{ registry_ext_port }}/ca.crt"
    remote_src: true
    mode:       '0644'

# โ”€โ”€ Install htpasswd tool โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Install apache2-utils (ฮณฮนฮฑ htpasswd)
  ansible.builtin.apt:
    name:  apache2-utils
    state: present
  when: ansible_facts['os_family'] == "Debian"

- name: Install httpd-tools (ฮณฮนฮฑ htpasswd)
  ansible.builtin.dnf:
    name:  httpd-tools
    state: present
  when: ansible_facts['os_family'] == "RedHat"

- name: Install apache2-utils (openSUSE)
  community.general.zypper:
    name:  apache2-utils
    state: present
  when: ansible_facts['os_family'] == "Suse"
EOF

tasks/auth.yml

cat > ~/ansible/roles/registry/tasks/auth.yml << 'EOF'
---
# ============================================================
# Registry Authentication โ€” htpasswd
# ============================================================

# โ”€โ”€ ฮ”ฮทฮผฮนฮฟฯ…ฯฮณฮฏฮฑ htpasswd ฮฑฯฯ‡ฮตฮฏฮฟฯ… โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Create htpasswd file
  ansible.builtin.file:
    path:  "{{ registry_auth_dir }}/htpasswd"
    state: touch
    mode:  '0600'
    owner: root
  when: registry_users | length == 0

# โ”€โ”€ ฮ ฯฮฟฯƒฮธฮฎฮบฮท users โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Add registry users
  ansible.builtin.command:
    cmd: >
      htpasswd
      {% if not htpasswd_stat.stat.exists %}
      -Bbc
      {% else %}
      -Bb
      {% endif %}
      {{ registry_auth_dir }}/htpasswd
      {{ item.username }}
      {{ item.password }}
  loop: "{{ registry_users }}"
  loop_control:
    label: "{{ item.username }}"
  no_log: true
  register: htpasswd_result
  vars:
    htpasswd_stat:
      stat:
        exists: true

- name: Check htpasswd file
  ansible.builtin.stat:
    path: "{{ registry_auth_dir }}/htpasswd"
  register: htpasswd_file

- name: Set htpasswd permissions
  ansible.builtin.file:
    path:  "{{ registry_auth_dir }}/htpasswd"
    mode:  '0600'
    owner: root
  when: htpasswd_file.stat.exists

- name: Show auth status
  ansible.builtin.debug:
    msg: >
      Registry auth:
      {{ 'โœ… ' + (registry_users | length | string) + ' users configured'
         if registry_users | length > 0
         else 'โš ๏ธ No users configured!' }}
EOF

tasks/deploy.yml

cat > ~/ansible/roles/registry/tasks/deploy.yml << 'EOF'
---
# ============================================================
# Registry Deployment ฮผฮต Docker Compose
# ============================================================

# โ”€โ”€ Render compose file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Render registry docker-compose.yml
  ansible.builtin.template:
    src:  registry-compose.yml.j2
    dest: "{{ registry_base_dir }}/docker-compose.yml"
    mode: '0644'

# โ”€โ”€ Render nginx config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Render nginx configuration
  ansible.builtin.template:
    src:  nginx-registry.conf.j2
    dest: "{{ registry_base_dir }}/nginx.conf"
    mode: '0644'

# โ”€โ”€ Pull images โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Pull registry images
  community.docker.docker_image:
    name:   "{{ item.name }}"
    tag:    "{{ item.tag }}"
    source: pull
  loop:
    - { name: registry, tag: "2"          }
    - { name: nginx,    tag: "1.25.3-alpine" }
  loop_control:
    label: "{{ item.name }}:{{ item.tag }}"

# โ”€โ”€ Deploy stack โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Deploy registry stack
  community.docker.docker_compose_v2:
    project_src:  "{{ registry_base_dir }}"
    project_name: "{{ registry_name }}"
    state:        present
  register: registry_deploy

# โ”€โ”€ Wait for registry โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Wait for registry to start
  community.docker.docker_container_info:
    name: "{{ registry_name }}_registry"
  register: reg_info
  until: >
    reg_info.container is defined and
    reg_info.container.State.Running == true
  retries: 12
  delay:   5

# โ”€โ”€ Wait for nginx โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Wait for nginx proxy to start
  community.docker.docker_container_info:
    name: "{{ registry_name }}_nginx"
  register: nginx_info
  until: >
    nginx_info.container is defined and
    nginx_info.container.State.Running == true
  retries: 6
  delay:   5

# โ”€โ”€ Setup garbage collection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Deploy registry GC script
  ansible.builtin.copy:
    dest:  /usr/local/bin/registry-gc.sh
    mode:  '0755'
    owner: root
    content: |
      #!/bin/bash
      # Registry Garbage Collection
      echo "=== Registry GC $(date) ==="
      docker compose \
          -f {{ registry_base_dir }}/docker-compose.yml \
          -p {{ registry_name }} \
          exec -T registry \
          registry garbage-collect \
          /etc/docker/registry/config.yml
      echo "=== GC complete ==="
  when: registry_gc_enabled

- name: Schedule registry GC
  ansible.builtin.cron:
    name:   "Registry garbage collection"
    minute: "{{ registry_gc_schedule.split()[0] }}"
    hour:   "{{ registry_gc_schedule.split()[1] }}"
    day:    "{{ registry_gc_schedule.split()[2] }}"
    month:  "{{ registry_gc_schedule.split()[3] }}"
    weekday: "{{ registry_gc_schedule.split()[4] }}"
    user:   root
    job:    "/usr/local/bin/registry-gc.sh >> /var/log/registry-gc.log 2>&1"
    state:  present
  when: registry_gc_enabled
EOF

templates/registry-compose.yml.j2

cat > ~/ansible/roles/registry/templates/registry-compose.yml.j2 << 'EOF'
{# Private Registry Docker Compose #}
{# Managed by Ansible โ€” DO NOT EDIT! #}

version: "3.8"

services:

  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  # REGISTRY โ€” Storage Backend
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  registry:
    image: {{ registry_image }}
    container_name: {{ registry_name }}_registry
    restart: unless-stopped
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
      REGISTRY_HTTP_ADDR:                        0.0.0.0:5000
      REGISTRY_HTTP_SECRET:                      {{ vault_registry_http_secret }}
      {% if registry_auth_enabled %}
      REGISTRY_AUTH:                             htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM:             "Registry Realm"
      REGISTRY_AUTH_HTPASSWD_PATH:              /auth/htpasswd
      {% endif %}
      REGISTRY_STORAGE_DELETE_ENABLED:           "true"
    volumes:
      - {{ registry_data_dir }}:/var/lib/registry
      - {{ registry_auth_dir }}:/auth:ro
      - /etc/localtime:/etc/localtime:ro
    networks:
      - registry_net
    deploy:
      resources:
        limits:
          memory: {{ registry_memory }}
          cpus:   "{{ registry_cpus }}"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test:         ["CMD", "wget", "-q", "-O", "/dev/null", "--no-check-certificate", "http://localhost:5000/v2/"]
      interval:     30s
      timeout:      10s
      retries:      3
      start_period: 10s

  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  # NGINX โ€” SSL Proxy
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  nginx:
    image: {{ nginx_image }}
    container_name: {{ registry_name }}_nginx
    restart: unless-stopped
    ports:
      - "{{ registry_ext_port }}:443"
    volumes:
      - {{ registry_base_dir }}/nginx.conf:/etc/nginx/nginx.conf:ro
      - {{ registry_certs_dir }}:/etc/nginx/certs:ro
      - {{ registry_logs_dir }}:/var/log/nginx
      - /etc/localtime:/etc/localtime:ro
    networks:
      - registry_net
    depends_on:
      registry:
        condition: service_healthy
    deploy:
      resources:
        limits:
          memory: {{ nginx_memory }}
          cpus:   "{{ nginx_cpus }}"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test:         ["CMD", "wget", "-q", "-O", "/dev/null", "--no-check-certificate", "https://localhost/v2/"]
      interval:     30s
      timeout:      10s
      retries:      3
      start_period: 15s

networks:
  registry_net:
    name: {{ registry_name }}_net
    driver: bridge
    internal: false
EOF

templates/nginx-registry.conf.j2

cat > ~/ansible/roles/registry/templates/nginx-registry.conf.j2 << 'EOF'
{# Nginx config for Docker Registry #}
{# Managed by Ansible โ€” DO NOT EDIT! #}

user nginx;
worker_processes 1;
pid /var/run/nginx.pid;

events {
    worker_connections 512;
}

http {
    include      /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log /var/log/nginx/registry_access.log;
    error_log  /var/log/nginx/registry_error.log warn;

    # โ”€โ”€ Large image uploads โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    client_max_body_size 0;            # โ† no limit ฮณฮนฮฑ images!
    chunked_transfer_encoding on;

    server {
        listen 443 ssl;
        server_name {{ registry_hostname }};

        # โ”€โ”€ SSL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        ssl_certificate     /etc/nginx/certs/registry.crt;
        ssl_certificate_key /etc/nginx/certs/registry.key;
        ssl_protocols       TLSv1.2 TLSv1.3;
        ssl_ciphers         HIGH:!aNULL:!MD5;
        ssl_session_cache   shared:SSL:10m;
        ssl_session_timeout 10m;

        # โ”€โ”€ Security headers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        add_header Strict-Transport-Security "max-age=31536000" always;
        add_header X-Content-Type-Options    "nosniff"          always;
        add_header X-Frame-Options           "DENY"             always;

        # โ”€โ”€ Registry proxy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        location /v2/ {
            # โ”€โ”€ Auth check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            {% if registry_auth_enabled %}
            auth_basic           "Docker Registry";
            auth_basic_user_file /etc/nginx/certs/../../../auth/htpasswd;
            {% endif %}

            # โ”€โ”€ Proxy settings โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            proxy_pass                        http://registry:5000;
            proxy_set_header  Host            $host;
            proxy_set_header  X-Real-IP       $remote_addr;
            proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header  X-Forwarded-Proto $scheme;

            # โ”€โ”€ Timeouts ฮณฮนฮฑ ฮผฮตฮณฮฌฮปฮฑ images โ”€โ”€โ”€โ”€
            proxy_connect_timeout  300;
            proxy_send_timeout     300;
            proxy_read_timeout     300;

            # โ”€โ”€ Buffer settings โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            proxy_buffering            off;
            proxy_request_buffering    off;
        }

        # โ”€โ”€ Health check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        location /health {
            access_log off;
            return 200 "OK\n";
            add_header Content-Type text/plain;
        }
    }

    # โ”€โ”€ HTTP โ†’ HTTPS redirect โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    server {
        listen 80;
        server_name {{ registry_hostname }};
        return 301 https://$host$request_uri;
    }
}
EOF

tasks/verify.yml

cat > ~/ansible/roles/registry/tasks/verify.yml << 'EOF'
---
# ============================================================
# Registry Verification
# ============================================================

# โ”€โ”€ Basic connectivity โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Check registry API (internal)
  ansible.builtin.uri:
    url:              "http://localhost:5000/v2/"
    status_code:      [200, 401]
    validate_certs:   false
  register:     reg_api_check
  ignore_errors: true

- name: Check registry API (external SSL)
  ansible.builtin.uri:
    url:            "https://localhost:{{ registry_ext_port }}/v2/"
    status_code:    [200, 401]
    validate_certs: false
  register:     reg_ssl_check
  ignore_errors: true

# โ”€โ”€ Authenticated check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Check registry ฮผฮต credentials
  ansible.builtin.uri:
    url:            "https://localhost:{{ registry_ext_port }}/v2/"
    status_code:    200
    validate_certs: false
    url_username:   "{{ registry_users[0].username }}"
    url_password:   "{{ registry_users[0].password }}"
    force_basic_auth: true
  register:      reg_auth_check
  when:          registry_users | length > 0
  no_log:        true
  ignore_errors: true

# โ”€โ”€ Test push & pull โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Login ฯƒฯ„ฮฟ private registry
  community.docker.docker_login:
    registry_url:   "https://{{ registry_hostname }}:{{ registry_ext_port }}"
    username:       "{{ registry_users[0].username }}"
    password:       "{{ registry_users[0].password }}"
    tls_verify:     false
  no_log:  true
  when:    registry_users | length > 0
  register: login_result

- name: Pull test image
  community.docker.docker_image:
    name:   alpine
    tag:    "3.19"
    source: pull
  when: login_result is not failed

- name: Tag ฮณฮนฮฑ private registry
  community.docker.docker_image:
    name:       "alpine:3.19"
    repository: "{{ registry_hostname }}:{{ registry_ext_port }}/test/alpine"
    tag:        "3.19"
    source:     local
  when: login_result is not failed

- name: Push test image ฯƒฯ„ฮฟ registry
  community.docker.docker_image:
    name:   "{{ registry_hostname }}:{{ registry_ext_port }}/test/alpine"
    tag:    "3.19"
    source: local
    push:   true
    tls_verify: false
  when:    login_result is not failed
  register: push_test

- name: Delete local test image
  community.docker.docker_image:
    name:         "{{ registry_hostname }}:{{ registry_ext_port }}/test/alpine"
    tag:          "3.19"
    state:        absent
    force_absent: true
  when: login_result is not failed

- name: Pull test image ฮฑฯ€ฯŒ registry
  community.docker.docker_image:
    name:       "{{ registry_hostname }}:{{ registry_ext_port }}/test/alpine"
    tag:        "3.19"
    source:     pull
    tls_verify: false
  when:    login_result is not failed
  register: pull_test

- name: Logout
  community.docker.docker_login:
    registry_url: "https://{{ registry_hostname }}:{{ registry_ext_port }}"
    username:     "{{ registry_users[0].username }}"
    state:        absent
  no_log: true
  when:   registry_users | length > 0

# โ”€โ”€ List catalog โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Get registry catalog
  ansible.builtin.uri:
    url:            "https://localhost:{{ registry_ext_port }}/v2/_catalog"
    status_code:    200
    validate_certs: false
    url_username:   "{{ registry_users[0].username }}"
    url_password:   "{{ registry_users[0].password }}"
    force_basic_auth: true
  register:     catalog
  when:         registry_users | length > 0
  no_log:       true
  ignore_errors: true

# โ”€โ”€ Report โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Registry Verification Report
  ansible.builtin.debug:
    msg:
      - "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—"
      - "โ•‘   REGISTRY VERIFICATION              โ•‘"
      - "โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ"
      - "โ•‘ Host   : {{ registry_hostname }}"
      - "โ•‘ Port   : {{ registry_ext_port }}"
      - "โ•‘ SSL    : {{ 'โœ… Enabled' if registry_ssl_enabled else 'โŒ Disabled' }}"
      - "โ•‘ Auth   : {{ 'โœ… Enabled' if registry_auth_enabled else 'โŒ Disabled' }}"
      - "โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ"
      - "โ•‘ CHECKS:                              โ•‘"
      - "โ•‘ API    : {{ 'โœ… OK' if not reg_api_check.failed  else 'โŒ Failed' }}"
      - "โ•‘ SSL    : {{ 'โœ… OK' if not reg_ssl_check.failed  else 'โŒ Failed' }}"
      - "โ•‘ Auth   : {{ 'โœ… OK' if not reg_auth_check.failed else 'โŒ N/A'    }}"
      - "โ•‘ Push   : {{ 'โœ… OK' if not push_test.failed      else 'โŒ Failed' }}"
      - "โ•‘ Pull   : {{ 'โœ… OK' if not pull_test.failed      else 'โŒ Failed' }}"
      - "โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ"
      - "โ•‘ Catalog: {{ catalog.json.repositories | default([]) | join(', ') | default('empty') }}"
      - "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"
EOF

handlers/main.yml

cat > ~/ansible/roles/registry/handlers/main.yml << 'EOF'
---
- name: Restart registry
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ registry_base_dir }}/docker-compose.yml
      -p {{ registry_name }}
      restart registry
  ignore_errors: "{{ ansible_check_mode }}"

- name: Reload nginx
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ registry_base_dir }}/docker-compose.yml
      -p {{ registry_name }}
      exec -T nginx nginx -s reload
  ignore_errors: "{{ ansible_check_mode }}"
EOF

Vault Variables ฮณฮนฮฑ Registry

cat > /tmp/registry_vault.yml << 'EOF'
---
vault_registry_http_secret: "RegistrySecret@2026#SuperLong!"
vault_registry_admin_pass:  "AdminPass@2026!"
vault_registry_user_pass:   "UserPass@2026!"
EOF

# ฮ ฯฮฟฯƒฮธฮฎฮบฮท ฯƒฯ„ฮฟ ฯ…ฯ€ฮฌฯฯ‡ฮฟฮฝ vault
ansible-vault edit \
    ~/ansible/inventory/group_vars/all/vault.yml
# โ†’ ฮ ฯฯŒฯƒฮธฮตฯƒฮต ฯ„ฮฑ ฯ€ฮฑฯฮฑฯ€ฮฌฮฝฯ‰

Deployment Playbook

cat > ~/ansible/playbooks/private-registry.yml << 'EOF'
---
# ============================================================
# Private Docker Registry Deployment
# ============================================================

- name: Deploy Private Docker Registry
  hosts: "{{ target | default('all_managed') }}"
  become: true
  gather_facts: true

  pre_tasks:

    - name: Registry deployment info
      ansible.builtin.debug:
        msg:
          - "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
          - "Private Docker Registry Deployment"
          - "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
          - "Host: {{ inventory_hostname }}"
          - "IP  : {{ ansible_facts['default_ipv4']['address'] }}"
          - "Port: {{ registry_ext_port | default(443) }}"
          - "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
      tags: always

    - name: Open firewall port (Debian/ferm)
      ansible.builtin.lineinfile:
        path:   /etc/ferm/ferm.conf
        regexp: ".*{{ registry_ext_port | default(443) }}.*"
        line:   "  proto tcp dport {{ registry_ext_port | default(443) }} ACCEPT;"
        insertafter: "chain INPUT"
      when:
        - ansible_facts['os_family'] == "Debian"
      notify: Restart ferm
      ignore_errors: true

  roles:
    - role: registry
      vars:
        registry_hostname:    "{{ inventory_hostname }}"
        registry_ext_port:    443
        registry_auth_enabled: true
        registry_users:
          - username: admin
            password: "{{ vault_registry_admin_pass }}"
          - username: deploy
            password: "{{ vault_registry_user_pass }}"
        registry_ssl_self_signed: true
        registry_gc_enabled:  true
      tags: registry

  post_tasks:

    - name: Registry Access Info
      ansible.builtin.debug:
        msg:
          - "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—"
          - "โ•‘   REGISTRY DEPLOYMENT COMPLETE! ๐ŸŽ‰   โ•‘"
          - "โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ"
          - "โ•‘ Registry URL:                        โ•‘"
          - "โ•‘ {{ inventory_hostname }}:443"
          - "โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ"
          - "โ•‘ Usage:                               โ•‘"
          - "โ•‘ docker login {{ inventory_hostname }}"
          - "โ•‘ docker tag myapp:1.0 {{ inventory_hostname }}/myapp:1.0"
          - "โ•‘ docker push {{ inventory_hostname }}/myapp:1.0"
          - "โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ"
          - "โ•‘ Note: Self-signed cert โ†’ add to      โ•‘"
          - "โ•‘ /etc/docker/certs.d/ on clients!     โ•‘"
          - "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"
      tags: always

  handlers:
    - name: Restart ferm
      ansible.builtin.service:
        name:  ferm
        state: restarted
      when: ansible_facts['os_family'] == "Debian"
      ignore_errors: "{{ ansible_check_mode }}"
EOF

ฮ•ฮบฯ„ฮญฮปฮตฯƒฮท

# โ”€โ”€ Syntax check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
ansible-playbook playbooks/private-registry.yml \
    --syntax-check

# โ”€โ”€ Dry run โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
ansible-playbook playbooks/private-registry.yml \
    --check \
    --limit nextcloud

# โ”€โ”€ Full deployment โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
ansible-playbook playbooks/private-registry.yml \
    --limit nextcloud \
    -v

# โ”€โ”€ ฮœฯŒฮฝฮฟ setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
ansible-playbook playbooks/private-registry.yml \
    --tags setup \
    --limit nextcloud

# โ”€โ”€ ฮœฯŒฮฝฮฟ verify โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
ansible-playbook playbooks/private-registry.yml \
    --tags verify \
    --limit nextcloud

Client Configuration

# ฮฃฮต ฮบฮฌฮธฮต Docker client ฯ€ฮฟฯ… ฯ‡ฯฮทฯƒฮนฮผฮฟฯ€ฮฟฮนฮตฮฏ ฯ„ฮฟ registry:

# โ”€โ”€ Copy certificate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
sudo mkdir -p /etc/docker/certs.d/nextcloud:443
sudo scp ansible@nextcloud:/opt/registry/certs/registry.crt \
    /etc/docker/certs.d/nextcloud:443/ca.crt

# โ”€โ”€ Login โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
docker login nextcloud:443
# Username: admin
# Password: ***

# โ”€โ”€ Push โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
docker tag nginx:alpine nextcloud:443/nginx:alpine
docker push nextcloud:443/nginx:alpine

# โ”€โ”€ Pull โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
docker pull nextcloud:443/nginx:alpine

ฮฃฯฮฝฮฟฯˆฮท D5.3

Private Registry ฮผฮต Ansible
โ”œโ”€โ”€ Components:
โ”‚   โ”œโ”€โ”€ registry:2  โ†’ image storage (internal)
โ”‚   โ””โ”€โ”€ nginx       โ†’ SSL proxy (external :443)
โ”‚
โ”œโ”€โ”€ Security:
โ”‚   โ”œโ”€โ”€ SSL: self-signed certificate
โ”‚   โ”œโ”€โ”€ Auth: htpasswd (bcrypt)
โ”‚   โ””โ”€โ”€ Internal network (registry_net)
โ”‚
โ”œโ”€โ”€ Setup:
โ”‚   โ”œโ”€โ”€ Directories: data, auth, certs, logs
โ”‚   โ”œโ”€โ”€ SSL cert: openssl req -newkey rsa:4096
โ”‚   โ””โ”€โ”€ htpasswd: htpasswd -Bbc
โ”‚
โ”œโ”€โ”€ Features:
โ”‚   โ”œโ”€โ”€ Garbage collection (cron)
โ”‚   โ”œโ”€โ”€ Storage delete enabled
โ”‚   โ””โ”€โ”€ Pull-through cache ready
โ”‚
โ””โ”€โ”€ Client:
    โ”œโ”€โ”€ Copy cert โ†’ /etc/docker/certs.d/
    โ”œโ”€โ”€ docker login
    โ””โ”€โ”€ docker push/pull