A modern IP Address Management (IPAM) platform built with Go, React, and PostgreSQL.
Replace spreadsheet-based IP tracking with a structured, API-first system.
- Backend: Go (Fiber)
- Frontend: React (Vite)
- Database: PostgreSQL
- Deployment: Docker Compose + GitHub Container Registry
You do not need to clone this repository or build images locally. The backend and frontend images are published to GitHub Container Registry; only the Compose file is required on the host.
mkdir padduck
cd padduck
curl -fsSLO https://raw.githubusercontent.com/lima3w/padduck/main/docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/lima3w/padduck/main/.env.example -o .env
# Edit .env and set POSTGRES_PASSWORD to a strong value before continuing
docker compose pull
docker compose up -dPOSTGRES_PASSWORD is required — the stack will not start without it. On
first startup, the backend creates a persistent MFA encryption key in
./data/backend/mfa-encryption-key if MFA_ENCRYPTION_KEY is not set.
Open http://localhost:3000 and log in as admin. The generated password is
printed to the backend log on first boot.
Configuration is read from environment variables. Docker Compose will also read a local .env file for variable interpolation if one is present — it is not created automatically.
| Variable | Default | Description |
|---|---|---|
POSTGRES_USER |
padduck |
PostgreSQL username |
POSTGRES_PASSWORD |
required | PostgreSQL password — must be set in .env before first run |
POSTGRES_DB |
padduck |
PostgreSQL database name |
DATABASE_URL |
derived | Overrides the individual PostgreSQL variables |
ADMIN_PASSWORD |
(generated) | Initial admin password; printed to logs on first boot if unset |
RESET_ADMIN_PASSWORD |
false |
Force-reset the admin password on next boot |
MFA_ENCRYPTION_KEY |
generated if unset | Optional override; 64 hex characters; generate with openssl rand -hex 32 |
SESSION_COOKIE_SECURE |
auto |
auto marks session cookies secure when behind HTTPS; set true or false to override |
FRONTEND_PORT |
3000 |
Host port the UI is exposed on |
FRONTEND_BIND |
127.0.0.1 |
Network interface the frontend port binds to. Defaults to loopback; place a TLS-terminating reverse proxy in front for production. Set to 0.0.0.0 only with additional network-level access control |
IMAGE_TAG |
1.34.10 |
Pinned release version (GHCR tags have no v prefix). To upgrade, set IMAGE_TAG=<new> in .env, then run docker compose pull && docker compose up -d |
Update checks can be enabled under Admin Settings → Updates. The backend checks the GitHub releases API automatically — no configuration required.
Full documentation is on the GitHub Wiki, including:
- Installation Guide
- Configuration
- User Guide
- API Documentation
- Troubleshooting
- v1 → v2 Migration Guide
- API Compatibility Policy
- Timestamps must be written in UTC. The schema uses
TIMESTAMP(without time zone) columns: pgx stores atime.Time's wall-clock digits as-is and reads them back as UTC, so any local-time value written to the database — or passed as a SQL query parameter — is wrong by the host's UTC offset. Always usetime.Now().UTC()for values that reach the database. The repository package enforces this with a test (repository/utc_guard_test.go); service-layer code must apply the same rule when constructing times for repository calls. Read-side comparisons against scanned values (time.Now().After(row.ExpiresAt)) compare instants and are safe either way.
Releases are deliberate — pushing to main does not publish anything.
To cut a release:
- In the release PR, add a
## vX.Y.Zsection at the top ofCHANGELOG.md(the changelog gate enforces this on every PR). - Merge to
main. - When ready to release, tag that commit and push the tag:
git tag vX.Y.Z && git push origin vX.Y.Z
The tag push triggers release.yml, which runs the full test suite (backend,
frontend, e2e) and only then publishes the GHCR images and the GitHub release.
Multiple PRs can land on main between releases; they all ship in the next
tag. Image tags on GHCR have no v prefix (1.31.27).