Wiredoor Ingress as a Service on Ubuntu 24.04 on Azure User Guide
Overview
Wiredoor is a self-hosted ingress-as-a-service platform. It solves the problem of reaching a service that has no public address: an application on a home lab, a machine behind carrier-grade NAT, a Kubernetes cluster with no load balancer, an IoT gateway on a customer site. Instead of forwarding ports or opening a firewall inwards, you install the Wiredoor CLI on the private machine and it dials outbound to this server over a WireGuard tunnel. Wiredoor then publishes the service through its own nginx reverse proxy on a public domain or path, with TLS, access control and monitoring, and traffic travels back down the encrypted tunnel. Nothing inbound is ever opened on the private network.
The cloudimg image installs Docker CE from the official Docker repository and runs Wiredoor 1.7.3 as a container pinned by digest. The container bundles nginx, the control-plane API, WireGuard, oauth2-proxy and certbot under supervisord, so there is no separate host web server to configure. A unique admin password and the public WireGuard endpoint are generated on the first boot of every VM, and the image itself ships with no credentials and no key material of any kind. Backed by 24/7 cloudimg support.
What is included:
- Wiredoor 1.7.3 (
ghcr.io/wiredoor/wiredoor:v1.7.3, Apache-2.0) running as a container managed bysystemd+ Docker - Docker CE preinstalled from the official Docker apt repository
- A WireGuard tunnel on UDP
51820, the public endpoint auto-detected on first boot - HTTP
80(redirecting to HTTPS) and HTTPS443ingress, plus the optional TCP service range32760-32767 - A web admin dashboard for nodes, domains, services and access logs
- A unique admin password generated on first boot and recorded in a root-only file
- Automatic Let's Encrypt certificates for public domains, and a self-signed certificate for IP-based access
- Persistent IP forwarding and WireGuard module loading, so the tunnel survives a reboot
docker.service+wiredoor.serviceas systemd units, enabled and active- 24/7 cloudimg support
Security posture of this image
Because Wiredoor's whole purpose is to publish private services to the internet, the appliance itself ships closed:
- No baked key material. Wiredoor generates the WireGuard server private key, the JWT signing secret, the self-signed TLS keypair and the QUIC host key lazily on first container start, inside the data volume. The image ships that volume empty, so every key is unique to your VM.
- No default administrator. The admin credential lives only in
/opt/wiredoor/.env, which does not exist in the image. Wiredoor 1.7.3 refuses to start if the administrator password is missing or is still the upstream sample value. - Nothing serves before lockdown.
wiredoor.serviceis gated on a bootstrap marker that first boot creates only after your per-VM secret has been written, so the dashboard and the ingress cannot answer a request until the VM has its own credentials. - No pre-seeded peers. There are no WireGuard peers or client configurations in the image; you register your own nodes.
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting point. Because Wiredoor is the entry point for all inbound traffic, the VM needs a public IP address (or a DNS name pointing at it) that your private nodes and your users can reach.
NSG inbound rules: allow 22/tcp from your management network, 80/tcp and 443/tcp for ingress and for Let's Encrypt validation, and 51820/udp for the WireGuard tunnel - without the UDP rule the dashboard works while no node can connect. Add 32760-32767/tcp only if you intend to expose raw TCP services.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Wiredoor by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22), HTTP (80) and HTTPS (443). The WireGuard UDP 51820 rule is added after creation in Step 2. Then Review + create -> Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name wiredoor \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
# Open ingress (TCP 80/443) and the WireGuard tunnel (UDP 51820)
az vm open-port --resource-group <your-rg> --name wiredoor --port 80 --priority 1010
az vm open-port --resource-group <your-rg> --name wiredoor --port 443 --priority 1020
az vm open-port --resource-group <your-rg> --name wiredoor --port 51820 --priority 1030
WireGuard is a UDP protocol - make sure the 51820/udp rule actually reaches the VM, or the dashboard will load while no node can establish a tunnel.
Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>
Step 4 - Confirm the services and the tunnel are up
systemctl is-active docker.service wiredoor.service
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
docker exec -u root wiredoor wg show
ss -tuln | grep -E ':(80|443|51820)\b' | awk '{print $1, $5}' | sort -u
Both units report active, the container runs the digest-pinned image, and wg show reports the wg0 interface listening on port 51820 with a public key generated on this VM:
active
active
NAMES IMAGE STATUS
wiredoor ghcr.io/wiredoor/wiredoor:v1.7.3 Up 4 minutes
interface: wg0
public key: CHCDuCmLi5HWqSGkjN70RfpqYz8f7RSAn3aWjtEUiy8=
private key: (hidden)
listening port: 51820
tcp 0.0.0.0:443
tcp 0.0.0.0:80
tcp [::]:443
tcp [::]:80
udp 0.0.0.0:51820
udp [::]:51820
wg show must be run as root inside the container - the container's own unprivileged user cannot read the interface.

Step 5 - Retrieve your admin password and tunnel endpoint
The admin password is generated uniquely on the first boot of your VM and written to a root-only file:
sudo sed -E 's/^(WIREDOOR_PASSWORD=).*/\1********/' /root/wiredoor-credentials.txt | head -8
The password is masked here only so it stays out of this page; on your VM the real value is on that line.
# Wiredoor - generated on first boot by wiredoor-firstboot.service
# These credentials are unique to this VM. Store them somewhere safe.
WIREDOOR_URL=https://20.169.218.14/
WIREDOOR_USERNAME=admin@wiredoor.local
WIREDOOR_PASSWORD=********
WIREGUARD_ENDPOINT=20.169.218.14:51820/udp
Read the whole file (including the notes at the bottom) with sudo cat /root/wiredoor-credentials.txt.
WIREDOOR_USERNAME is the sign-in name. WIREGUARD_ENDPOINT is the address your private nodes will dial, auto-detected on first boot; if this VM sits behind a NAT gateway or you use a DNS name, correct VPN_HOST in /opt/wiredoor/.env and restart the service.

Step 6 - Verify the credentials and the ingress
The image ships a checker that proves the per-VM credential works end to end and that anonymous and forged requests are rejected:
sudo /usr/local/sbin/wiredoor-credential-check.sh
OK wiredoor credential round-trip verified (401 anon, 401 forged, wrong-pw rejected, per-VM password mints a working JWT)
You can confirm the ingress separately - port 80 redirects to HTTPS, the dashboard answers on 443, and the API rejects an unauthenticated call:
printf 'http -> %s\n' "$(curl -s -o /dev/null -w '%{http_code}' -m 10 http://127.0.0.1/)"
printf 'https -> %s\n' "$(curl -ks -o /dev/null -w '%{http_code}' -m 10 https://127.0.0.1/)"
printf 'api (no token) -> %s\n' "$(curl -ks -o /dev/null -w '%{http_code}' -m 10 https://127.0.0.1/api/auth/me)"
http -> 301
https -> 200
api (no token) -> 401
Sign-in is rate limited to 5 attempts per 10 minutes per source IP. The checker above uses two of them. If you see
Too many attempts, try again later, wait for the window to clear rather than retrying.

Step 7 - Sign in to the dashboard
Open https://<vm-public-ip>/ in a browser and sign in with the WIREDOOR_USERNAME and WIREDOOR_PASSWORD from Step 5.
Until you attach a public domain, the dashboard is served with a self-signed certificate, so your browser will warn once. That is expected for IP-based access; Step 11 replaces it with a real Let's Encrypt certificate.

Once signed in, the dashboard summarises your nodes, the services you have published and recent activity.

Step 8 - Install the Wiredoor CLI on your private machine
Everything so far ran on the Wiredoor server. The remaining registration steps run on the private machine whose service you want to publish - your home server, a VM in a private subnet, an on-site gateway. It needs no public IP and no inbound firewall rule; it only needs to be able to reach this server outbound on UDP 51820.
curl -s https://www.wiredoor.net/install-wiredoor-cli.sh | sh
Packages are also published on the wiredoor-cli releases page.
Step 9 - Register the node and publish a service
Still on the private machine, log in to your Wiredoor server and register this machine as a node:
wiredoor login --url=https://<vm-public-ip>
The CLI prompts for the admin credentials from Step 5, asks for a name for this node, registers it, and brings up the WireGuard tunnel automatically. Then publish a local service - here an application listening on port 3000:
wiredoor http myapp --domain app.<your-domain> --port 3000
The domain must have a DNS A record pointing at your Wiredoor server's public IP. Wiredoor requests a Let's Encrypt certificate for it, adds an nginx location, and forwards matching requests down the tunnel to 127.0.0.1:3000 on the private machine. To expose a raw TCP service instead, use wiredoor tcp <name> --port <port>, which allocates a port from the 32760-32767 range.
Make the tunnel survive a reboot of the private machine:
systemctl enable --now wiredoor
Step 10 - Confirm the node and its published services
Back in the dashboard, Nodes lists every registered machine and its tunnel state. The image arrives with one built-in local node, which is how the dashboard itself is published through Wiredoor's own ingress - so a working dashboard is already proof the reverse-proxy path is healthy.

Opening a node shows the services published through it, with their domain or path, backend target and status. This is the view to check when a published service returns an error - it shows whether Wiredoor believes the service is up and where it is forwarding.

Step 11 - Set your contact email before enabling public certificates
Wiredoor reuses the administrator email as the Let's Encrypt registration contact. The image sets a placeholder sign-in name, which is not a deliverable address, so set your own address before you request a certificate for a public domain - otherwise expiry warnings go nowhere.
sudo sed -i "s/^ADMIN_EMAIL=.*/ADMIN_EMAIL=<your-email>/" /opt/wiredoor/.env
sudo systemctl restart wiredoor.service
Changing ADMIN_EMAIL changes the sign-in username too, so use the new address the next time you sign in. The password is unchanged.
Maintenance
Restart or stop the stack. The service is a thin systemd wrapper around Docker Compose:
sudo systemctl restart wiredoor.service
sudo systemctl status wiredoor.service --no-pager | head -12
Where things live.
sudo ls -l /opt/wiredoor/
docker-compose.yml is the stack definition, .env holds this VM's configuration and admin secret (0600 root:root), wiredoor-data/ is the SQLite database, WireGuard configuration and generated keys, and wiredoor-certbot/ holds Let's Encrypt certificates.
Back up. Stop the stack, archive the data and certificate directories, then start it again:
sudo systemctl stop wiredoor.service
sudo tar czf /root/wiredoor-backup-$(date +%F).tar.gz -C /opt/wiredoor wiredoor-data wiredoor-certbot .env
sudo systemctl start wiredoor.service
Treat that archive as a secret: it contains the WireGuard server private key, the JWT signing secret and your admin password.
Upgrade. Wiredoor is pinned by tag in /opt/wiredoor/docker-compose.yml. To move to a newer release, take a backup first, edit the image: line to the new tag, then pull and recreate:
sudo sed -i 's|wiredoor:v1.7.3|wiredoor:v<new-version>|' /opt/wiredoor/docker-compose.yml
sudo docker compose -f /opt/wiredoor/docker-compose.yml pull
sudo systemctl restart wiredoor.service
Read the upstream release notes before upgrading - Wiredoor 1.5.0 changed the container to run as a non-root user, which needed a one-time ownership fix.
Change the tunnel endpoint. If the auto-detected address is wrong (a NAT gateway, or you would rather nodes dial a DNS name), edit VPN_HOST in /opt/wiredoor/.env and restart. Existing node configurations are regenerated with the new endpoint.
Restrict access to a published service. Wiredoor can allow or block by public CIDR when you publish a service, and can put an OAuth2 provider in front of it. Note that IP restrictions must use public ranges - the service is reachable from the internet, so filtering on private ranges has no effect.
Troubleshooting
The dashboard does not load. Check that first boot finished and the container is up:
systemctl is-active wiredoor-firstboot.service wiredoor.service
sudo journalctl -u wiredoor-firstboot.service --no-pager | tail -20
A node cannot connect. This is almost always the UDP rule. Confirm the host is listening, then confirm your NSG allows 51820/udp from the node's network:
ss -uln | grep 51820
Sign-in returns "Too many attempts". Sign-in allows 5 attempts per 10 minutes per source IP. Wait for the window to clear.
A published service returns 502. Wiredoor reached the tunnel but not the backend. Check that the service is listening on the port you published, on the private machine, and that the node's tunnel is up in the Nodes view.
Inspect the container's own logs. All the bundled processes log through the container:
sudo docker logs wiredoor 2>&1 | tail -30
On a completely fresh data volume you may see one UNIQUE constraint failed: wg_interfaces.config line followed by Failed to bootstrap application in the first few seconds. That is an upstream start-up race between two of the bundled processes; supervisord restarts the loser immediately and the stack comes up clean. It is harmless, and only ever appears on the very first start.
Support
This image is published and maintained by cloudimg with 24/7 support. Wiredoor itself is open source under the Apache-2.0 licence; see the Wiredoor documentation and GitHub repository for upstream detail.