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

Blue infrastructure icon showing connected nodes around a server and circular deployment arrows

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

  1. 01

    Trigger

    A code change or pull request starts the relevant CI/CD workflow.

  2. 02

    Assign

    Azure DevOps assigns pipeline work to an available self-hosted agent.

  3. 03

    Validate

    The workflow checks code quality, types, tests, and build output.

  4. 04

    Review

    A pull request preview makes the change reviewable before release.

  5. 05

    Deploy

    A validated production change deploys to Azure Web Apps.

  6. 06

    Verify

    Pipeline results and service logs show the outcome of the release.

Application delivery

  1. 1Run checks on private agents
  2. 2Review a preview deployment
  3. 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 stepsShow
  1. 01

    Code change

    Starts the CI/CD workflow.

    triggers
  2. 02

    Azure DevOps

    Queues and tracks pipeline jobs.

    assigns
  3. 03

    Agent on my hardware

    Runs checks and deployment steps on my physical host.

    deploys
  4. 04

    Azure Web Apps

    Hosts the validated application release.

Tailscale to my host

3 connected stepsShow
  1. 01

    Remote operator

    Connects to the private infrastructure.

    connects
  2. 02

    Tailscale

    Provides the mesh VPN access path.

    routes access
  3. 03

    My physical host

    Runs the agent pool, RAID 5 storage, and operations services.

Restart alert to mail provider

5 connected stepsShow
  1. 01

    Restart check

    Detects a changed host boot ID and prepares an alert.

    submits
  2. 02

    msmtp

    Hands the message to Postfix on the private Docker bridge.

    internal SMTP
  3. 03

    Postfix relay

    Queues the alert and authenticates to Resend using its credential.

    authenticated SMTP over STARTTLS
  4. 04

    Resend

    Receives the alert as the configured SMTP provider.

    sends through
  5. 05

    AWS SES

    Resend-managed infrastructure for sending the email.

Host components

Inside my owned physical hostShow
  • 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.

Tech stack

Azure DevOps PipelinesSelf-hosted agentsAzure Web AppsTailscaleRAID 5Docker ComposeDockerAlpine LinuxBashmsmtpPostfixSMTPSTARTTLSResend SMTP (AWS SES backend)
← Back to projects