TL;DR
- You build a reusable Polycrate block that automates the deployment of Nginx and Let's Encrypt (via
community.general.certbot) on a Linux server – including a genericnginx.confas a Jinja2 template. - The block parameterizes domain, contact email, and upstream port through
block.config.*and can be used in any workspace with different values – without code duplication or wiki tinkering. - Polycrate containerizes Ansible and the entire toolchain: no local Ansible, no Python chaos, no individual setups – the same block runs identically on every developer workstation.
- Through a registry (
from: cargo.ayedo.cloud/ayedo/infra/nginx-letsencrypt:1.0.0inworkspace.poly), you share the block with colleagues: before pushing,nameinblock.polymust be the full OCI reference without a tag;polycrate blocks pushuses that name plusversionas the tag – sharing instead of screenshots. - ayedo supports teams in building such standardized, shareable automation blocks – from designing the block structure to operating a registry and integrating into existing processes.
The Real Problem: The Forgotten Nginx Config
Many admins know the scenario: An Nginx has been running on web01.acme-corp.com for years, the nginx.conf was "quickly" adjusted back then, Let's Encrypt was set up somewhere with certbot – and no one remembers exactly how.
- Why is the upstream port 8081?
- Why is a subdomain being redirected here?
- Which certbot option was it again?
Documentation, if any, is in a wiki entry that no longer matches reality. Automation consists of individual shell snippets that were only ever tried on this server.
With plain Ansible, this can be done better, but you know the hurdles:
- Managing local Ansible installation, Python versions, collections
- Maintaining, distributing, versioning roles and playbooks
- Colleagues need to understand Ansible CLI, inventories, and variable structures
In this post, I'll show you how to turn exactly this setup into a reusable Polycrate block that your colleagues can easily use – without needing to know the implementation.
From Server Config to Reusable Block
Polycrate builds directly on Ansible but solves two typical problems:
-
Dependency Problem:
Ansible runs in a container. No local installation, no Python zoo, no different version states in the team. The block brings everything it needs. -
Sharable Automation:
Your Nginx setup is no longer a fragile wiki article but a versioned block that resides in a registry. What you build, others can use immediately – with their own domains and ports.
We focus on a specific block:
- Installs Nginx and certbot on Ubuntu 22.04
- Obtains a Let's Encrypt certificate for a domain
- Deploys an
nginx.confbased on a Jinja2 template - Parameterizes:
block.config.domainblock.config.emailblock.config.upstream_port
Block Structure: Directory & block.poly
Starting point is an existing Polycrate workspace, e.g.:
# workspace.poly
name: acme-corp-automation
organization: acme
Our block is located in ./blocks/nginx-letsencrypt/ and contains:
block.polynginx-letsencrypt.yml(Ansible playbook)nginx.conf.j2(Jinja2 template)
Directory layout (overview)
How the workspace root and block directory relate (example workspace name acme-corp-automation):
acme-corp-automation/ # Workspace root
├── workspace.poly # Workspace config, including block instances
├── inventory.yml # Central Ansible inventory (all blocks)
├── blocks/
│ └── nginx-letsencrypt/ # Local block (directory name = template name in from:)
│ ├── block.poly # Block definition: version, config, actions
│ ├── nginx-letsencrypt.yml # Ansible playbook (deploy action)
│ └── nginx.conf.j2 # Jinja2 template for Nginx
└── … # other typical entries (e.g. artifacts/, secrets.poly)
Later, when you publish the block to a registry with a full OCI name, the same files usually live under a path that mirrors the registry name, e.g. blocks/registry.acme-corp.com/infra/nginx/ (see Pushing to a registry).
Complete block.poly
name: nginx-letsencrypt
version: 1.0.0
kind: generic
config:
domain: ""
email: ""
upstream_port: 8080
actions:
- name: deploy
playbook: nginx-letsencrypt.yml
Key points:
version: 1.0.0– we explicitly version the block.configdefines the parameters to be set later in the workspace:domain– FQDN likewww.acme-corp.comemail– contact email for Let's Encryptupstream_port– port of the backend that Nginx proxies to (e.g., an app on 8080)actionsdescribes which playbooks are available.
Withpolycrate run nginx-letsencrypt deploy, we execute the playbook.
Further details on block structure can be found in the official Block Concepts.
nginx.conf as Jinja2 Template
The core for reusability is the Nginx configuration as a template. Instead of fixed domain names and ports, we use Polycrate variables.
Complete nginx.conf.j2
# nginx.conf for {{ block.config.domain }} – generated by Polycrate
server {
listen 80;
server_name {{ block.config.domain }};
# HTTP → HTTPS Redirect
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ block.config.domain }};
ssl_certificate /etc/letsencrypt/live/{{ block.config.domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ block.config.domain }}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Optional HSTS – adjust if needed
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:{{ block.config.upstream_port }};
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;
}
}
What happens here:
{{ block.config.domain }}and{{ block.config.upstream_port }}come directly fromblock.configin the block – and are ultimately set in the workspace.- The certificate paths point to certbot's standard directories (
/etc/letsencrypt/live/...). - The configuration is idempotently deployable from Ansible – changes to the template lead to a consistent state.
Ansible Playbook for Nginx + Let's Encrypt
Now we connect everything in a playbook. Important note in Polycrate: The playbooks run in the container, but the target hosts are real servers from the inventory, not localhost.
Inventory in the Workspace
In the workspace root is inventory.yml (Polycrate sets ANSIBLE_INVENTORY automatically):
# inventory.yml
all:
hosts:
web01.acme-corp.com:
ansible_user: ubuntu
ansible_become: true
Complete Playbook nginx-letsencrypt.yml
---
- name: Deploy Nginx with Let's Encrypt
hosts: all
become: true
vars:
domain_name: "{{ block.config.domain }}"
contact_email: "{{ block.config.email }}"
upstream_port: "{{ block.config.upstream_port }}"
pre_tasks:
- name: Domain must be set
ansible.builtin.assert:
that:
- domain_name | length > 0
- contact_email | length > 0
fail_msg: "block.config.domain and block.config.email must be set in the workspace."
tasks:
- name: Update APT cache (Ubuntu/Debian)
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install Nginx and Certbot
ansible.builtin.apt:
name:
- nginx
- certbot
- python3-certbot
state: present
- name: Disable default site
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: reload nginx
- name: Obtain TLS certificate from Let's Encrypt (Standalone)
community.general.certbot:
email: "{{ contact_email }}"
domains:
- "{{ domain_name }}"
authenticator: standalone
agree_tos: true
installer: none
state: present
register: certbot_result
- name: Show Certbot output (Debug)
ansible.builtin.debug:
var: certbot_result
- name: Deploy Nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: "/etc/nginx/sites-available/{{ domain_name }}"
owner: root
group: root
mode: "0644"
notify: reload nginx
- name: Enable site
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ domain_name }}"
dest: "/etc/nginx/sites-enabled/{{ domain_name }}"
state: link
notify: reload nginx
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
enabled: true
Explanations:
hosts: all– all hosts frominventory.yml. Noconnection: local, because we manage real servers.community.general.certbotcomes from thecommunity.generalcollection. Polycrate can install this in the container without touching your local system (see Ansible Integration).- We use the
standaloneauthenticator: - Certbot briefly starts its own web server on port 80.
- Then we deploy the HTTPS Nginx configuration.
- Through
vars, we bind the Polycrate variables (block.config.*) to Ansible variables, simplifying reading in the playbook.
Using the Block in the Workspace
After building and polycrate blocks push, workspaces consume the block the same way as in the rest of this series: full OCI reference under cargo.ayedo.cloud (not only the short template name).
Referencing the Block in the Workspace
# workspace.poly
name: acme-corp-automation
organization: acme
blocks:
- name: webserver
from: cargo.ayedo.cloud/ayedo/infra/nginx-letsencrypt:1.0.0
config:
domain: "www.acme-corp.com"
email: "admin@acme-corp.com"
upstream_port: 8080
name: webserveris the instance name in the workspace.- The block lives in the registry at
cargo.ayedo.cloud. You do not need to runpolycrate blocks pullfirst: if the block is missing locally, Polycrate detects that when you runpolycrate run …and asks whether to install the block from the registry automatically. After that, the unpacked tree lives underblocks/cargo.ayedo.cloud/ayedo/infra/nginx-letsencrypt/. Infrom:use the full reference including the tag (block sharing via OCI, not only the localblocks/nginx-letsencrypt/folder). - Under
config, we override the default values fromblock.poly.
Execute Action
polycrate run webserver deploy
Polycrate:
- Starts a container with Ansible and all necessary tools.
- Loads
inventory.ymlfrom the workspace root. - Sets variables like
block.config.*and executes the playbook in the container. - Modifies the target host via SSH – the container itself remains ephemeral.
With plain Ansible, it would look like this:
ansible-playbook -i inventory.yml nginx-letsencrypt.yml \
-e "domain_name=www.acme-corp.com contact_email=admin@acme-corp.com upstream_port=8080"
And you would have to ensure that:
- Ansible is installed locally
- The correct Python version is running
- The
community.generalcollection is available
With Polycrate, these dependencies are part of the block or workspace and run in the container.
Pushing the Web Server Block to a Registry
Now comes the sharing argument: Instead of writing a wiki entry, you publish your block in a registry.
Important: Blocks with purely local names like nginx-letsencrypt or nginx cannot be pushed to a registry. polycrate blocks push expects name in block.poly to already be the full OCI reference without a version tag (host and path like a container image name without :tag). The version lives in the separate version: field and becomes the OCI tag on push.
Assuming you operate an internal Polycrate/OCI registry at registry.acme-corp.com.
Adjust block.poly for the registry
Before pushing, set the registry name and version as usual:
name: registry.acme-corp.com/infra/nginx
version: 1.0.0
kind: generic
config:
domain: ""
email: ""
upstream_port: 8080
actions:
- name: deploy
playbook: nginx-letsencrypt.yml
Recommended: mirror the name in the directory layout, e.g. blocks/registry.acme-corp.com/infra/nginx/ (see Best practices).
Push the block
polycrate blocks push registry.acme-corp.com/infra/nginx
There is only this one argument – the block name exactly as in block.poly (name, without tag). There is no second argument for a “destination URL”; Polycrate derives host, path, and tag from name and version.
What happens:
- Polycrate packages the block (including
block.poly, playbook, template) as an OCI artifact. - The artifact is uploaded as
registry.acme-corp.com/infra/nginx:1.0.0(name+versionas tag). - For a follow-up release, bump
versioninblock.poly(e.g.1.1.0) and push again.
In the workspace: pin the version
When others consume the block, they reference it with from: including a tag – best practice is to pin the block version when declaring the instance in the workspace, e.g. from: registry.acme-corp.com/infra/nginx:1.0.0 (see Registry documentation).
Colleague Uses the Block in Their Workspace
Your colleague has a different workspace, e.g., for an internal portal. They want to use the same Nginx/Let's Encrypt block, but with different values.
workspace.poly at the Colleague
# workspace.poly in the colleague's workspace
name: acme-portal-automation
organization: acme
blocks:
- name: portal-web
from: registry.acme-corp.com/infra/nginx:1.0.0
config:
domain: "portal.acme-corp.com"
email: "ops@acme-corp.com"
upstream_port: 9000
They don't need to:
- Read the playbook
- Understand the Nginx template syntax
- Know certbot options
They just set the configuration.