Infrastructure automation for deploying packtly — a self-hosted Debian package repository based on aptly and nginx, running as a rootless Podman container managed by systemd Quadlets.
packtly-infra/
├── ansible/ # Ansible playbook and roles
│ ├── packtly_setup.yml # Main playbook
│ └── roles/
│ ├── container_runtime/ # pip/podman setup
│ ├── packtly_secrets/ # GPG, SSH, API secret generation
│ ├── packtly_service/ # Container deploy, volume, Quadlet service
│ └── sshconfig/ # Local ~/.ssh/config.d entry
├── inventories/
│ ├── hosts.yml # Target hosts (generated, gitignored)
│ └── group_vars/
│ ├── dev/local.yml # Dev config (gitignored)
│ └── prod/local.yml # Prod config (gitignored)
├── packtly-infra/
│ ├── Containerfile # Container image definition
│ └── podman-compose.yml # Local dev compose setup
├── scripts/
│ ├── packtly_init.py # Interactive inventory setup wizard
│ ├── deploy # Ansible deploy wrapper
│ └── setup-ansible-inventory # Legacy bash inventory setup
└── justfile # Local dev workflow recipes
- Python 3.7+
- pipx
- Podman
- Ansible +
containers.podmancollection
Install everything:
./scripts/install-requirements
./scripts/install-ansibleRun the interactive wizard to create inventories/hosts.yml and encrypted group_vars:
./scripts/packtly_init.pyIt will prompt for:
- Environment name (
dev/prod) - Target host (hostname/IP, or
localhostfor local deploy) - Bootstrap SSH user
- Container method (
buildorpull) and registry settings - Service user, ports, GPG identity, API credentials
- Vault password (used to encrypt secrets via
ansible-vault)
./scripts/deploy dev # deploy to dev inventory group
./scripts/deploy prod # deploy to prod inventory groupThis runs the Ansible playbook against the specified --limit group.
ansible/packtly_setup.yml contains two plays:
| Play | User | Purpose |
|---|---|---|
| Bootstrap packtly host | bootstrap_user (become root) |
Container runtime, secrets, service user |
| Deploy packtly service | service_user |
Container image, volume, Quadlet unit, SSH config |
| Role | Description |
|---|---|
container_runtime |
Configures pip (optional proxy) |
packtly_secrets |
Builds setup container, generates GPG key, SSH key, API credentials, stores them locally under ansible/generated-secrets/<host>/ |
packtly_service |
Creates service user, deploys container image (build or pull), provisions Podman volume with public keys and htpasswd, registers Quadlet systemd unit |
sshconfig |
Writes ~/.ssh/config.d/packtly-<host> for SSH access to the running container |
inventories/hosts.yml is generated by the wizard and gitignored. Example:
all:
children:
prod:
hosts:
mars:
ansible_connection: ssh
ansible_host: mars
bootstrap_user: user
dev:
hosts:
localhost:
ansible_connection: local
ansible_python_interpreter: /usr/bin/python3inventories/group_vars/<env>/local.yml holds non-secret config:
service_user: packtly
container_name: packtly
container_method: build # or pull
container_image_local: packtly-infra:dev
datadir: packtly_data
web_port: "8080"
ssh_port: "2222"
gpg_user: yourname
gpg_email: user@example.com
api_user: admininventories/group_vars/<env>/vault.yml holds ansible-vault encrypted secrets:
ansible_become_password: ...
gpg_pass: ...
api_pass: ...
vault_container_registry_password: ...just # list available recipes
just all # full local setup: build, secrets, volume, start, create repos
just setup-image # build setup container image
just secrets-create # generate GPG, SSH, API credentials into .dev-secrets/
just volume-create # create Podman volume and Podman secrets
just volume-create recreate=true # recreate (destroys existing data)
just create-repos # import GPG key and create aptly repos
just service-logs # follow container logs
just clean # stop service, remove volume and images
just clean-all # clean + delete .dev-secrets/The container runs as a rootless Podman service under the packtly user:
- Quadlet unit:
~/.config/containers/systemd/packtly.container - Managed by:
systemctl --user(scope:service_user) - Ports: HTTP
8080→ aptly REST API + nginx repo, SSH2222→ dropbear - Volume:
packtly_data— persists aptly DB, repos, GPG keyrings
Check service status by switching to the service user via systemd-run:
sudo systemd-run --system --scope su - packtly -c "systemctl --user status packtly.service"
sudo systemd-run --system --scope su - packtly -c "journalctl --user -u packtly.service -n 100 -f --no-pager"On a remote host:
ssh packtly@<host> "systemctl --user status packtly.service"
ssh packtly@<host> "journalctl --user -u packtly.service -n 100 -f --no-pager"SSH into the running container:
ssh packtly-<hostname> # configured by sshconfig roleCreate /etc/apt/sources.list.d/packtly.sources:
Types: deb
URIs: http://<host>:8080
Suites: trixie-apollo
Components: main
Signed-By: /etc/apt/trusted.gpg.d/packtly.gpgImport the signing key:
curl http://<host>:8080/public/repo_signing.key \
| gpg --dearmor \
| sudo tee /etc/apt/trusted.gpg.d/packtly.gpg > /dev/nullThe aptly REST API is available at http://<host>:8080/api (basic auth):
curl -u admin:<password> http://<host>:8080/api/versionSSH into the running container to use the aptly CLI directly:
ssh packtly-<hostname>aptly repo show -with-packages trixie-apollo-contribRemove a specific package by exact name and architecture:
aptly repo remove trixie-apollo-contrib debhello_1.0.0_amd64Wildcard removal (all architectures of a version):
aptly repo remove trixie-apollo-contrib debhello_1.0.0_*Remove the source package:
aptly repo remove trixie-apollo-contrib debhello_1.0.0_sourceRemove a debug symbol package:
aptly repo remove trixie-apollo-contrib debhello-dbgsym_1.0.0_amd64After removing packages, republish the affected snapshot/repo so clients see the updated state.
- All secrets are generated inside the temporary setup container — nothing is generated on the control node.
- GPG private key and passphrase are stored as Podman secrets (
packtly_gpg_private_key,packtly_gpg_passphrase) — never written to the container filesystem. - Local generated secrets are stored under
ansible/generated-secrets/<host>/and are gitignored. - Vault variables are encrypted with
ansible-vaultand must be decrypted at deploy time with--ask-vault-pass.