Docker
A Conn3x circuit that installs Docker Engine from Docker’s own repository, keeps it running, and runs your Compose stacks. It’s the piece most homelab setups start with: get Docker on the box, then bring up Jellyfin, the *arr apps, a VPN container or a monitoring stack from a compose.yaml.
Each stack’s file lives in your configuration, so a change to it shows up in conn3x plan as a diff, and the apply runs docker compose up only when the file changed or a service stopped. That also means a stack’s file is in state, so keep passwords out of it: point Compose at an env_file or a secret file on the host instead.
module "docker" { source = "github.com/conn3xorg/circuit-docker?ref=v0.1.2"
hosts = { media = "192.168.1.20" } ssh_private_key_file = "~/.ssh/id_ed25519"
docker_users = ["james"] stacks = { media = file("${path.module}/stacks/media.yaml") }}With stacks/media.yaml next to it:
services: jellyfin: image: lscr.io/linuxserver/jellyfin:12.4ubu2604-ls57 ports: ["8096:8096"] volumes: - /srv/jellyfin/config:/config - /srv/media:/data/media restart: unless-stopped sonarr: image: lscr.io/linuxserver/sonarr:4.0.20.3014-ls1 ports: ["8989:8989"] volumes: - /srv/sonarr/config:/config - /srv/media:/data/media restart: unless-stoppedThen run conn3x init and conn3x apply. A second conn3x plan says No changes.
For hosts created in the same plan, pass host objects with the host key you generated, so Conn3x checks it without a known_hosts entry:
hosts = { for name, d in digitalocean_droplet.app : name => { address = d.ipv4_address host_key = tls_private_key.host[name].public_key_openssh }}To try it on a throwaway container first, see examples/local.
What it does
Section titled “What it does”On each host, in order:
- Adds Docker’s repository and signing key:
download.docker.com/linux/rhelon Rocky Linux, AlmaLinux and RHEL, and thefedora,ubuntuordebianone elsewhere. The keys are in files, copied from Docker’s site. - Installs
docker-ce,docker-ce-cli,containerd.io,docker-buildx-pluginanddocker-compose-plugin. - Writes
/etc/docker/daemon.jsonfromdaemon_config. - If the kernel can’t load
xt_addrtype, installskernel-modules-extrafor the running kernel. Docker’s NAT rules need it, and Rocky Linux 10 and other EL10 cloud images leave it out, so Docker fails to start without it. - Starts Docker now and at boot, and restarts it when
daemon_configchanges, before any stack runs. - Adds each of
docker_usersto thedockergroup. The users must already exist; the users circuit can create them. - For each stack, writes
/opt/stacks/NAME/compose.yamland runsdocker compose up -d --remove-orphans --waitin that directory.
A stack runs again when its file changed since the last successful run, or when one of its services isn’t running. --wait makes the apply fail if a service doesn’t start, or doesn’t turn healthy when it has a health check. Every service in a stack should keep running: one that runs once and exits looks stopped, so the stack would run again on every apply.
Before the circuit runs, the conn3x_host data source reads each host’s distribution over SSH. On Debian and Ubuntu it picks the apt suite from the release: Ubuntu 22.04, 24.04 and 26.04, and Debian 12 and 13. Another release fails the plan.
The circuit doesn’t open firewall ports for your stacks. Docker publishes ports through its own firewall rules, which bypass both ufw and firewalld: a published port answers from outside even when the host firewall doesn’t allow it. Publish on 127.0.0.1, as in "127.0.0.1:8080:80", for anything only the host should reach.
Removing a stack
Section titled “Removing a stack”Deleting a stack from stacks leaves it running. Run docker compose down in its directory first, then delete it.
Inputs
Section titled “Inputs”| Name | Type | Default | Description |
|---|---|---|---|
hosts |
any | Required | Hosts to configure, by name. Each value is an address, such as "203.0.113.10", or a host object such as { address = "203.0.113.10", host_key = "ssh-ed25519 AAAA..." }. A host object can also set user, port, private_key_file and jump_hosts. The two forms can be mixed. |
ssh_user |
string | "root" |
User to log in as. |
ssh_port |
number | 22 |
SSH port. |
ssh_private_key_file |
string | null |
Path to a private key. When null, Conn3x uses the SSH agent. |
ssh_known_hosts_file |
string | null |
known_hosts file to check host keys against. When null, ~/.ssh/known_hosts. Not used for a host object with host_key. |
ssh_become |
bool | false |
Run commands as root through sudo. Set it when ssh_user isn’t root. |
daemon_config |
any | {} |
Settings for /etc/docker/daemon.json, such as { log-driver = "json-file", log-opts = { max-size = "10m" } }. |
docker_users |
list(string) | [] |
Existing users to add to the docker group. |
stacks |
map(string) | {} |
Compose files to run, by stack name. Names use lowercase letters, digits, - and _. |
Outputs
Section titled “Outputs”| Name | Description |
|---|---|
stack_dirs |
Each stack’s directory on the hosts, such as { media = "/opt/stacks/media" }. |
Requirements
Section titled “Requirements”Requires Conn3x v0.6.4 or later and systemd on each host, with outbound HTTPS to download.docker.com and to the registries your stacks pull from.
Tested on
Section titled “Tested on”Rocky Linux 10, Fedora 45, Ubuntu 24.04 and Debian 13. CI applies examples/local to each on amd64, checks that the second plan has no changes, and fetches a page from the example stack from outside the host, before and after restarting Docker.
CI containers share the runner’s kernel, so they can’t catch a missing kernel module. The example was also applied by hand, alongside circuit-monitoring, circuit-firewall and four other circuits, to an Ubuntu 24.04 droplet and a Rocky Linux 10 droplet with SELinux enforcing. That run found the missing xt_addrtype on Rocky Linux, now fixed. On both, the stack answered, changing its compose file took effect in a single apply, a published port not allowed by ufw or firewalld still answered from outside, and after a reboot every container came back with no plan changes and no SELinux denials. The same checks passed on Fedora 44, with SELinux enforcing, and Debian 13 droplets, before and after a reboot.
License
Section titled “License”MPL-2.0. See LICENSE. Docker Engine itself is Apache-2.0.
Source
Section titled “Source”- Repository: conn3xorg/circuit-docker. This page covers v0.1.2; the repository’s tags list every release.
- Examples: examples/local.
- Related circuits: Users, Podman, k3s, kubeadm, MicroShift, Rancher, containerd, Nomad, Helm, Istio, Flux.