Owned infrastructure · DevOps
Self-Hosted Private Deployment Cloud
My own hardware runs Azure DevOps agents for validated releases to Azure Web Apps, with private access through Tailscale.
6 min read

I built this platform to run CI/CD jobs on hardware I own and physically manage. That gave me control over the execution environment, storage, and remote access, along with responsibility for keeping the host working.
The host runs an Azure DevOps agent pool, uses RAID 5 storage, and is reachable through Tailscale. Application releases go to Azure Web Apps. A separate Docker Compose stack handles scheduled work, persistent logs, and operational alerts.
From change to deployment
- 01
Trigger
A code change or pull request starts the relevant CI/CD workflow.
- 02
Assign
Azure DevOps assigns pipeline work to an available self-hosted agent.
- 03
Validate
The workflow checks code quality, types, tests, and build output.
- 04
Review
A pull request preview makes the change reviewable before release.
- 05
Deploy
A validated production change deploys to Azure Web Apps.
- 06
Verify
Pipeline results and service logs show the outcome of the release.
Application delivery
- 1Run checks on private agents
- 2Review a preview deployment
- 3Deploy validated release to Azure Web Apps
The pull request and production paths share CI checks. I update the separate Compose operations services with a manual build and start.
Architecture
The host is the boundary I manage. Azure DevOps schedules work for its agents, Azure Web Apps hosts application releases, and Tailscale provides private administration. The Compose alert route leaves the host through Resend, which uses AWS SES behind its service.
Azure DevOps to Azure Web Apps
4 connected stepsShowHide
- 01
Code change
Starts the CI/CD workflow.
triggers - 02
Azure DevOps
Queues and tracks pipeline jobs.
assigns - 03
Agent on my hardware
Runs checks and deployment steps on my physical host.
deploys - 04
Azure Web Apps
Hosts the validated application release.
Tailscale to my host
3 connected stepsShowHide
- 01
Remote operator
Connects to the private infrastructure.
connects - 02
Tailscale
Provides the mesh VPN access path.
routes access - 03
My physical host
Runs the agent pool, RAID 5 storage, and operations services.
Restart alert to mail provider
5 connected stepsShowHide
- 01
Restart check
Detects a changed host boot ID and prepares an alert.
submits - 02
msmtp
Hands the message to Postfix on the private Docker bridge.
internal SMTP - 03
Postfix relay
Queues the alert and authenticates to Resend using its credential.
authenticated SMTP over STARTTLS - 04
Resend
Receives the alert as the configured SMTP provider.
sends through - 05
AWS SES
Resend-managed infrastructure for sending the email.
Host components
Inside my owned physical hostShowHide
RAID 5 storage
Provides storage redundancy on my physical host.
Azure DevOps agent pool
Executes pipeline jobs assigned by Azure DevOps.
Automation container
Runs scheduled work and the host restart check under Docker Compose.
Host bind mounts
Persist operational logs and boot state across container recreation.
Private Docker bridge
Carries SMTP between the automation container and Postfix without a host port.
Postfix relay
Accepts alert mail, holds the upstream credential, and queues temporary failures.
What it does
Private agents run CI/CD while Compose supports the host.
The agent pool provides a consistent execution environment for pipeline jobs across my DevOps workflows. Azure DevOps schedules the jobs, while the validated applications run on Azure Web Apps.
Docker Compose runs host support services separately. Scheduled automation records its output, and a restart check submits an alert through the shared Postfix relay.
Storage and state
RAID 5 and bind mounts support persistent host operations.
The physical host uses RAID 5 storage. Bind mounts preserve operational logs and the last recorded boot ID across container recreation. Scripts and mail-client configuration are mounted read-only.
A reusable cron wrapper writes scheduled command output to both host and container logs while preserving each command's exit status. The automation image is built locally from Alpine, and the relay image is pinned.
Security and access
Tailscale handles remote access. Postfix holds the SMTP credential.
Tailscale provides the mesh VPN path for remote administration. The Postfix relay has no published host port, so scheduled containers submit mail across a private Docker bridge.
That internal SMTP hop trusts Docker network membership. Postfix holds the Resend credential in a private environment file and authenticates to Resend over STARTTLS, keeping the credential out of the scheduled workloads.
Delivery and operations
Checks and previews gate Azure releases. Compose updates are manual.
Pipeline checks cover linting, formatting, types, unit tests, and builds. Pull requests receive a preview, and validated production changes deploy to Azure Web Apps.
I build and start the Compose operations services manually. After an update, I inspect container state, logs, and the Postfix queue to check the host support path.
Testing and verification
Boot ID, logs, SMTP submission, and queue state expose failures.
I check container state, cron output, logs, and the Postfix queue to verify the operations stack. A manual SMTP test confirmed that the upstream provider accepted a message and the local queue emptied.
On startup, a check compares the current kernel boot ID with the last value saved on the host. A change submits a restart alert. The new ID is saved only after local mail submission succeeds, leaving a failed submission eligible for retry on a later start.
Decisions and lessons
Control over execution brings maintenance and security responsibility.
Using my own hardware gave me control over the agent environment, but made host updates, storage, private access, and failure signals my responsibility. Persistent logs and an inspectable mail queue became part of operating the platform, not optional extras.
Keeping Compose operations separate from application pipelines made each release path easier to reason about. A shared Postfix relay kept the provider credential in one place, while the internal SMTP path still depended on trusted network membership. The boot ID check reported a restart, not its cause.