This commit is contained in:
2026-09-18 20:07:04 +02:00
parent 958e22734f
commit 442ddf8816
88 changed files with 2501 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
---
# Install Docker CE (rootful) from the official Docker apt repository.
# ponytail: rootful Docker only for this PoC; rootless is explicitly deferred (see plan.md).
- name: Gather OS release facts
ansible.builtin.setup:
gather_subset:
- distribution
- name: Install prerequisite packages
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gnupg
state: present
update_cache: true
- name: Create keyrings directory
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
- name: Download Docker GPG key
ansible.builtin.get_url:
url: "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg"
dest: /etc/apt/keyrings/docker.asc
mode: "0644"
force: false
- name: Add Docker apt repository
ansible.builtin.deb822_repository:
name: docker
types: deb
uris: "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}"
suites: "{{ ansible_facts['distribution_release'] }}"
components: stable
architectures: "{{ 'amd64' if ansible_facts['architecture'] == 'x86_64' else ansible_facts['architecture'] }}"
signed_by: /etc/apt/keyrings/docker.asc
state: present
- name: Update apt cache after adding Docker repository
ansible.builtin.apt:
update_cache: true
- name: Install Docker packages
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
- name: Ensure Docker service is enabled and running
ansible.builtin.systemd_service:
name: docker
enabled: true
state: started
+5
View File
@@ -0,0 +1,5 @@
---
woodpecker_domain: "ci.poc.mehl.mx"
zot_domain: "registry.ci.poc.mehl.mx"
zot_port: 5000
letsencrypt_email: "dqdpoq@dwqiofiw.de"
+5
View File
@@ -0,0 +1,5 @@
---
- name: Restart nginx
ansible.builtin.systemd_service:
name: nginx
state: restarted
+32
View File
@@ -0,0 +1,32 @@
---
# Two-phase TLS setup: nginx first serves plain HTTP (for the certbot
# webroot challenge), then certbot obtains real certs, then the TLS
# vhosts (proxying to Woodpecker and Zot) are deployed and nginx reloaded.
- name: Ensure certbot webroot directory exists
ansible.builtin.file:
path: "{{ certbot_webroot }}"
state: directory
mode: "0755"
- name: Include geerlingguy.nginx role (HTTP-only vhosts for ACME challenge)
ansible.builtin.include_role:
name: geerlingguy.nginx
- name: Include geerlingguy.certbot role (obtain certificates)
ansible.builtin.include_role:
name: geerlingguy.certbot
- name: Deploy TLS vhost for Woodpecker
ansible.builtin.template:
src: woodpecker.conf.j2
dest: "/etc/nginx/sites-enabled/{{ woodpecker_domain }}.conf"
mode: "0644"
notify: Restart nginx
- name: Deploy TLS vhost for Zot
ansible.builtin.template:
src: zot.conf.j2
dest: "/etc/nginx/sites-enabled/{{ zot_domain }}.conf"
mode: "0644"
notify: Restart nginx
@@ -0,0 +1,36 @@
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name {{ woodpecker_domain }};
ssl_certificate /etc/letsencrypt/live/{{ woodpecker_domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ woodpecker_domain }}/privkey.pem;
client_max_body_size 64m;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
# Required for Woodpecker's websocket-based log streaming.
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
listen 80;
listen [::]:80;
server_name {{ woodpecker_domain }};
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://$host$request_uri;
}
}
+36
View File
@@ -0,0 +1,36 @@
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name {{ zot_domain }};
ssl_certificate /etc/letsencrypt/live/{{ zot_domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ zot_domain }}/privkey.pem;
# Large blob/layer uploads for container images and OCI artifacts.
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:{{ zot_port }};
proxy_http_version 1.1;
proxy_set_header Host $http_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;
# Ensure a well-formed Content-Length reaches Zot instead of
# chunked transfer encoding, which Zot rejects on blob uploads.
proxy_request_buffering on;
}
}
server {
listen 80;
listen [::]:80;
server_name {{ zot_domain }};
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://$host$request_uri;
}
}
+35
View File
@@ -0,0 +1,35 @@
---
# HTTP-only vhosts, used for the initial certbot webroot challenge and
# permanently for redirecting to HTTPS afterwards.
nginx_vhosts:
- server_name: "{{ woodpecker_domain }}"
filename: "{{ woodpecker_domain }}.conf"
root: "/var/www/letsencrypt"
extra_parameters: |
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://$host$request_uri;
}
- server_name: "{{ zot_domain }}"
filename: "{{ zot_domain }}.conf"
root: "/var/www/letsencrypt"
extra_parameters: |
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://$host$request_uri;
}
certbot_create_if_missing: true
certbot_create_method: webroot
certbot_webroot: /var/www/letsencrypt
certbot_admin_email: "{{ letsencrypt_email }}"
certbot_certs:
- domains:
- "{{ woodpecker_domain }}"
- domains:
- "{{ zot_domain }}"
+5
View File
@@ -0,0 +1,5 @@
---
woodpecker_version: "v3.18.1"
woodpecker_host: "https://ci.poc.mehl.mx"
woodpecker_gitea_url: "https://src.mehl.mx"
woodpecker_admin_user: "mxmehl"
+6
View File
@@ -0,0 +1,6 @@
---
- name: Restart woodpecker
ansible.builtin.command:
cmd: docker compose up -d --force-recreate
chdir: /opt/woodpecker
changed_when: true
+29
View File
@@ -0,0 +1,29 @@
---
# ponytail: UID/GID 1000 is hardcoded to match the woodpecker-server/agent
# images' built-in "woodpecker" user (checked via /etc/passwd in the image).
# If a future image version changes this UID, ownership here must follow.
- name: Create woodpecker directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
owner: "1000"
group: "1000"
loop:
- /opt/woodpecker
- /opt/woodpecker/server-data
- /opt/woodpecker/agent-config
- name: Deploy woodpecker compose file
ansible.builtin.template:
src: compose.yaml.j2
dest: /opt/woodpecker/compose.yaml
mode: "0600"
notify: Restart woodpecker
- name: Start woodpecker via docker compose
ansible.builtin.command:
cmd: docker compose up -d
chdir: /opt/woodpecker
changed_when: true
@@ -0,0 +1,34 @@
services:
woodpecker-server:
image: woodpeckerci/woodpecker-server:{{ woodpecker_version }}
restart: unless-stopped
ports:
- "127.0.0.1:8000:8000"
volumes:
- /opt/woodpecker/server-data:/var/lib/woodpecker/
environment:
- WOODPECKER_OPEN=false
- WOODPECKER_HOST={{ woodpecker_host }}
- WOODPECKER_GITEA=true
- WOODPECKER_GITEA_URL={{ woodpecker_gitea_url }}
- WOODPECKER_GITEA_CLIENT={{ woodpecker_gitea_client }}
- WOODPECKER_GITEA_SECRET={{ woodpecker_gitea_secret }}
- WOODPECKER_AGENT_SECRET={{ woodpecker_agent_secret }}
- WOODPECKER_ADMIN={{ woodpecker_admin_user }}
# ponytail: only this specific plugin image is allow-listed for
# privileged mode (needed to build images), not privileged mode in
# general - keep this list as narrow as possible.
- WOODPECKER_PLUGINS_PRIVILEGED=woodpeckerci/plugin-docker-buildx
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:{{ woodpecker_version }}
command: agent
restart: unless-stopped
depends_on:
- woodpecker-server
volumes:
- /opt/woodpecker/agent-config:/etc/woodpecker
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WOODPECKER_SERVER=woodpecker-server:9000
- WOODPECKER_AGENT_SECRET={{ woodpecker_agent_secret }}
+8
View File
@@ -0,0 +1,8 @@
---
zot_version: "v2.1.6"
zot_port: 5000
zot_admin_user: zotadmin
# ponytail: PoC-only default password, overridden via inventory/vault for real use.
zot_admin_password: "changeme-poc-only"
zot_retention_keep_count: 10
zot_retention_keep_within: "720h"
+6
View File
@@ -0,0 +1,6 @@
---
- name: Restart zot
ansible.builtin.command:
cmd: docker compose up -d --force-recreate
chdir: /opt/zot
changed_when: true
+52
View File
@@ -0,0 +1,52 @@
---
# Deploy Zot OCI registry via Docker Compose, with htpasswd basic auth
# and a simple retention policy.
# ponytail: single zot instance, no TLS termination here (assumed behind
# plain HTTP for the PoC); add a reverse proxy with TLS before any real use.
- name: Create zot directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- /opt/zot
- /opt/zot/data
- /opt/zot/auth
- name: Ensure apache2-utils (htpasswd) is installed
ansible.builtin.apt:
name: apache2-utils
state: present
- name: Check if htpasswd file already exists
ansible.builtin.stat:
path: /opt/zot/auth/htpasswd
register: zot_htpasswd_stat
- name: Generate htpasswd file for zot user
ansible.builtin.command:
cmd: >-
htpasswd -cbB /opt/zot/auth/htpasswd
{{ zot_admin_user }} {{ zot_admin_password }}
when: not zot_htpasswd_stat.stat.exists
changed_when: true
- name: Deploy zot configuration
ansible.builtin.template:
src: config.json.j2
dest: /opt/zot/config.json
mode: "0644"
notify: Restart zot
- name: Deploy zot compose file
ansible.builtin.template:
src: compose.yaml.j2
dest: /opt/zot/compose.yaml
mode: "0644"
- name: Start zot via docker compose
ansible.builtin.command:
cmd: docker compose up -d
chdir: /opt/zot
changed_when: true
+11
View File
@@ -0,0 +1,11 @@
services:
zot:
image: ghcr.io/project-zot/zot-linux-amd64:{{ zot_version }}
restart: unless-stopped
ports:
- "127.0.0.1:{{ zot_port }}:5000"
volumes:
- /opt/zot/config.json:/etc/zot/config.json:ro
- /opt/zot/auth/htpasswd:/etc/zot/htpasswd:ro
- /opt/zot/data:/var/lib/registry
command: serve /etc/zot/config.json
+42
View File
@@ -0,0 +1,42 @@
{
"distSpecVersion": "1.1.1",
"storage": {
"rootDirectory": "/var/lib/registry",
"retention": {
"dryRun": false,
"delay": "24h",
"policies": [
{
"repositories": ["**"],
"deleteReferrers": false,
"deleteUntagged": true,
"keepTags": [
{
"mostRecentlyPushedCount": {{ zot_retention_keep_count }},
"pushedWithin": "{{ zot_retention_keep_within }}"
}
]
}
]
}
},
"http": {
"address": "0.0.0.0",
"port": "5000",
"auth": {
"htpasswd": {
"path": "/etc/zot/htpasswd"
}
},
"accessControl": {
"repositories": {
"**": {
"defaultPolicy": ["read", "create", "update", "delete"]
}
}
}
},
"log": {
"level": "info"
}
}