PodCD is a lightweight GitOps controller for managing containers with Docker or Podman through native systemd units.
The desired state of your infrastructure is stored in a Git repository. PodCD continuously reconciles the local machine with the configuration declared in Git and applies changes automatically.
PodCD brings GitOps concepts such as reconciliation, drift detection, ConfigMaps, Secrets, Init Tasks and SOPS-encrypted secrets to standalone Linux hosts without requiring Kubernetes.
PodCD provides GitOps workflows for single Linux hosts without requiring Kubernetes.
Compared to Kubernetes:
- No cluster management
- No API server
- No etcd
- Native systemd integration
- Lower resource consumption
Compared to docker-compose:
- Continuous reconciliation
- Drift detection
- Git-based desired state
- Secret management
- Resource dependency tracking
- GitOps-driven deployments
- Docker and Podman support
- Native systemd unit generation
- Declarative infrastructure resources
- Container lifecycle management
- Network management
- Volume management
- ConfigMap support
- Secret management
- Init Tasks support
- SOPS + Age encrypted secrets
- Environment variable injection
- Resource dependency management
- Resource-hash based reconciliation
- Automatic drift detection
- Root and rootless operation
- Lightweight, single-node focused
- No Kubernetes dependency
Git Repository
│
▼
Source
│
▼
Desired State
│
▼
Reconciler
│
├── Networks
├── Volumes
├── ConfigMaps
├── Secrets
├── Init Tasks
└── Containers
│
▼
Runtime State
│
▼
systemd
│
▼
Docker / Podman
podcddpodcd reqspodcd pubkey ssh
podcd pubkey agepodcd validatepodcd syncpodcd statusgo build -o podcd ./cmd/podcd
go build -o podcdd ./cmd/podcddConfiguration file:
/etc/podcd/config.yaml
Data directory:
/var/lib/podcd
Configuration file:
~/.config/podcd/config.yaml
Data directory:
~/.local/share/podcd
Example controller configuration:
source:
repoUrl: git@github.com:me/gitops.git
version: main
runtime:
type: auto
controller:
interval: 10s
server:
logLevel: info| Field | Description |
|---|---|
source.repoUrl |
Git repository containing desired state resources |
source.version |
Git branch, tag, or revision to reconcile |
runtime.type |
docker, podman, or auto |
controller.interval |
Reconciliation interval |
server.logLevel |
Logging level (debug, info, warn, error) |
gitops/
├── networks/
│ └── backend.yaml
├── volumes/
│ └── postgres-data.yaml
├── configmaps/
│ └── app-config.yaml
├── secrets/
│ └── database-secret.yaml
└── containers/
├── postgres.yaml
└── api.yaml
kind: Container
name: api
init:
- name: migrate
image: ghcr.io/example/api:latest
command:
- migrate
- up
once: true
image: ghcr.io/example/api:latest
networks:
- name: backend
volumes:
- name: app-data
target: /app/data
env:
APP_ENV: production
configMaps:
- name: app-config
target: /app/config.env
secrets:
- name: db-secret
target: /run/secrets/db-secret
ports:
- hostPort: 8080
containerPort: 8080kind: Network
name: backend
driver: bridge
attachable: truekind: Volume
name: postgres-datakind: ConfigMap
name: app-config
data: |
LOG_LEVEL=info
APP_NAME=demokind: Secret
name: database-secret
data: |
DB_USER=app
DB_PASSWORD=changemekind: Secret
name: database-secret
encryptedData:
DB_USER: ENC[AES256_GCM,...]
DB_PASSWORD: ENC[AES256_GCM,...]
sops:
age:
- recipient: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSecrets remain encrypted in Git and are decrypted only during reconciliation.
kind: Container
name: postgres
image: postgres:17
env:
POSTGRES_DB: app
POSTGRES_USER: app
ports:
- hostPort: 5432
containerPort: 5432
volumes:
- name: postgres-data
target: /var/lib/postgresql/data
networks:
- name: backendInit tasks are short-lived containers executed before the main container starts.
Typical use cases include:
- Database migrations
- Initial data loading
- Permission fixes
- One-time bootstrap operations
- Waiting for dependencies
Example:
kind: Container
name: api
init:
- name: migrate
image: ghcr.io/example/api:latest
command:
- migrate
- up
once: true
image: ghcr.io/example/api:latestInit tasks execute in declaration order.
If an init task fails, the main container is not started and reconciliation is marked as failed.
When once: true is specified, PodCD records successful execution and does not execute the task again during future reconciliations.
The task is executed again only if its specification changes and therefore produces a different resource hash.
kind: Container
name: app
init:
- name: wait-database
image: postgres:17
command:
- sh
- -c
- |
until pg_isready -h postgres -U app; do
sleep 2
done
- name: migrate
image: ghcr.io/example/app:latest
configMaps:
- name: app-config
target: /app/config.env
secrets:
- name: database-secret
target: /run/secrets/database-secret
command:
- migrate
- up
once: true
image: ghcr.io/example/app:latestPodCD materializes ConfigMaps and Secrets under its runtime storage directory and mounts them into containers using read-only bind mounts.
kind: Container
name: api
image: ghcr.io/example/api:latest
configMaps:
- name: app-config
target: /app/config.env
secrets:
- name: database-secret
target: /run/secrets/database-secretpodcd reqs
podcd repo add
podcd repo delete
podcd repo list
podcd pubkey ssh
podcd pubkey age
podcd validate
podcd status
podcd sync
podcd version
- Resource hashes are recalculated
- Drift is detected
- Dependencies are reconciled
- Init tasks are executed when required
- Runtime resources are updated when needed
- systemd units are regenerated when required
- State is reconciled automatically
PodCD automatically reconciles resources in dependency order.
For example:
- Networks are created before containers
- Volumes are created before containers
- ConfigMaps are materialized before containers start
- Secrets are decrypted before containers start
- Init Tasks execute and complete successfully before the main container starts
podcd.io/managed=true
podcd.io/name=<resource-name>
podcd.io/resource-hash=<hash>
- Simple GitOps for single hosts
- Native Linux integration through systemd
- Secure secret management with SOPS and Age
- Support Docker and Podman equally
- Minimal operational footprint
- No Kubernetes complexity
MIT