Host www2.guydev.ca in Docker via WSL
Step-by-step runbook to Dockerize the GuyStore ASP.NET Core 9 MVC app, run it inside WSL, and expose it publicly through the existing Cloudflare Tunnel — without touching the live IIS site on localhost:80.
Architecture Overview
Before writing a single command, understand how all the pieces connect. Your Windows machine runs two parallel web stacks sharing one Cloudflare Tunnel.
IIS already owns Windows port 80. The Docker container internally listens on port 80 (standard for HTTP in a container), but Docker Desktop maps it to Windows port 5080. Cloudflare Tunnel then connects to localhost:5080. No IIS reconfiguration needed.
Port Conflict Strategy
Your machine has exactly one conflict to avoid: IIS holds port 80 on all Windows network interfaces. Here is the full picture.
| Port (Windows) | Owner | Purpose | Status |
|---|---|---|---|
80 | IIS (GuyStore) | Serves guydev.ca / www.guydev.ca | KEEP — do not touch |
5080 | Docker Desktop | Maps to container port 80 (www2) | NEW — will be created |
443 | IIS (optional) | HTTPS — cloudflared handles TLS | Irrelevant for tunnel |
Mapping container port 80 to Windows port 80 will fail silently or crash the container because IIS holds that binding. Always use -p 5080:80 (Windows 5080 → container 80).
Run this in PowerShell before continuing. If nothing is returned, the port is available.
Expected output: blank (no output means nothing is using 5080).
Install WSL 2
WSL 2 is the Linux kernel that Docker Desktop uses under the hood. Even though you interact with Docker Desktop on Windows, your containers run inside this Linux VM.
2.1 — Enable WSL 2
Open PowerShell as Administrator and run:
This command enables the required Windows features, downloads the Linux kernel update, and installs Ubuntu as the default distro. Reboot when prompted.
Run wsl --status — if it reports Default Version: 2 you are already on WSL 2 and can skip the install. Just run wsl --update to ensure the kernel is current.
2.2 — Verify after reboot
Expected: Default Version: 2 and your Ubuntu distro listed with state Running or Stopped.
wsl --status reports Default Version: 2. Ubuntu distro is listed. Machine has been rebooted.
Install Docker Desktop
Docker Desktop for Windows provides a GUI, CLI tools, and integrates with WSL 2 so containers run natively in Linux — not in a slow Hyper-V VM.
3.1 — Download and install
Download from https://www.docker.com/products/docker-desktop and run the installer. On the installation options screen, ensure "Use WSL 2 instead of Hyper-V" is checked (it is by default).
Reboot after installation.
3.2 — Configure WSL integration
Open Docker Desktop → Settings → Resources → WSL Integration. Enable integration for your Ubuntu distro (toggle it on). Click Apply & Restart.
3.3 — Verify Docker is working
Expected: version string, then the "Hello from Docker!" message. Docker is confirmed working.
docker run hello-world printed "Hello from Docker!" with no errors.
Publish the GuyStore App
You need a self-contained published output of GuyStore to copy into the Docker image. "Self-contained" means the .NET runtime is bundled — the Docker image does not need .NET pre-installed separately.
4.1 — Choose your publish method
Section 5 uses a multi-stage Dockerfile that compiles and publishes inside Docker itself — you do not need to manually publish from Visual Studio. This is the industry-standard approach. Skip §4.1 and go directly to §5 if you want to use that approach (recommended).
If you prefer to publish from Visual Studio first (simpler for first-timers):
4.2 — Publish from Visual Studio 2022
Right-click the GuyStore project → Publish → Folder. Set the target folder to:
C:\Guy\Docker\guystore-publish
Click Publish. Visual Studio runs dotnet publish -c Release and drops all files into that folder.
4.3 — Confirm output
Expected: you see GuyStore.dll, GuyStore.exe, web.config, wwwroot\ folder, and other DLLs.
Write the Dockerfile
The template used here is the official Microsoft multi-stage ASP.NET Core 9 Dockerfile — the most widely used pattern for .NET web apps. It compiles in a SDK image, then copies only the output into a slim runtime image.
The final image only contains the runtime (~220 MB), not the full SDK (~800 MB). It also means your Docker image always reflects a clean build — no leftover obj/ or bin/ folders.
5.1 — Create the project folder structure
In PowerShell, create a Docker build context folder:
5.2 — Copy your GuyStore source into the build context
The multi-stage build needs your source code. Copy the entire GuyStore solution into the Docker build folder. Adjust the source path to match your actual project location:
5.3 — Create the Dockerfile
Create this file at C:\Guy\Docker\www2\Dockerfile
Open Notepad or VS Code, paste the content below, and save as Dockerfile (no extension):
The ENTRYPOINT uses GuyStore.dll. If your project's assembly name is different (check the .csproj <AssemblyName> tag), update this line. After a successful build you can also run docker run --rm www2 ls /app to see the exact filename.
5.4 — Create a .dockerignore file
Create C:\Guy\Docker\www2\.dockerignore to keep the build fast:
Build & Run the Container
6.1 — Build the Docker image
Open PowerShell and navigate to the build context folder, then build:
First build takes 2–5 minutes (downloading base images). Subsequent builds are fast due to layer caching.
Expected final lines:
6.2 — Run the container
The critical line: port 5080 on Windows maps to port 80 inside the container. The --name flag makes it easy to stop/start later.
| Flag | Meaning |
|---|---|
-d | Detached mode — runs in background |
--name www2 | Container name for easy management |
-p 5080:80 | Windows port 5080 → container port 80 |
--restart unless-stopped | Auto-restarts on crash or Docker Desktop restart |
6.3 — Check container status
Expected: one row showing www2 with status Up N seconds and 0.0.0.0:5080->80/tcp.
docker ps shows www2 with status Up. Port mapping shows 0.0.0.0:5080->80/tcp.
Verify the App Locally
Before touching Cloudflare, confirm the container is serving your app correctly from Windows.
7.1 — Test via curl
Expected output: 200
7.2 — Test via browser
Open http://localhost:5080 in your browser. You should see the GuyStore MVC app loading correctly — same layout, same pages as the IIS-hosted version.
Run docker logs www2 to see the app's startup output. Common issues: (a) the app binds to localhost only — fix by ensuring ASPNETCORE_URLS=http://+:80 is set in the Dockerfile (already included above); (b) the app crashes on startup — check for missing connection strings in appsettings.
7.3 — Check app logs
Expected: ASP.NET Core startup messages including Now listening on: http://[::]:80 and Application started.
Browser at http://localhost:5080 shows the GuyStore app. Curl returns 200. Docker logs show Application started.
Add Tunnel Ingress Rule
The existing Cloudflare Tunnel (GuyStoreHome) needs one new ingress rule to route www2.guydev.ca to the Docker container at localhost:5080.
8.1 — Locate the config.yml file
The config.yml you set up previously is at:
C:\Windows\System32\config\systemprofile\.cloudflared\config.yml
The cloudflared Windows service runs as the SYSTEM account, so its config and credentials must live under the SYSTEM profile. This was established in the original runbook session.
8.2 — Open the file
You must use an elevated editor. Run this in PowerShell as Admin to open in Notepad:
8.3 — Current config structure
Your existing config.yml looks something like this (hostnames may vary slightly):
8.4 — Add the www2 rule
Insert the new rule before the catch-all service: http_status:404 line. The order matters — cloudflared matches top-to-bottom.
Save the file (Ctrl+S) and close Notepad.
8.5 — Validate the config
Run in CMD as Administrator (not PowerShell — the (x86) path trips up PowerShell):
Expected output: Validating rules from C:\...\config.yml followed by each hostname listed and OK. No errors.
ingress validate shows all three hostnames (guydev.ca, www.guydev.ca, www2.guydev.ca) listed as OK. No validation errors.
Add DNS Record in Cloudflare
The DNS CNAME record tells Cloudflare's edge where to send traffic for www2.guydev.ca. It must point to the tunnel, not to your IP.
9.1 — Navigate to DNS Records
dash.cloudflare.com → guydev.ca → DNS → Records
9.2 — Add the record
Click Add record and fill in exactly:
| Field | Value |
|---|---|
| Type | CNAME |
| Name | www2 |
| Target | YOUR-TUNNEL-ID.cfargotunnel.com |
| Proxy status | Proxied (orange cloud ON) |
| TTL | Auto |
It is in your config.yml at the top — the tunnel: value. You can also find it at one.dash.cloudflare.com → Networks → Tunnels → GuyStoreHome — copy the UUID from the tunnel detail page.
Click Save.
DNS Records list now shows a www2 CNAME row with orange cloud (Proxied). Target ends in .cfargotunnel.com.
Restart Cloudflared & Verify
The cloudflared Windows service must be restarted to pick up the new ingress rule in config.yml.
10.1 — Restart the service
Run in CMD as Administrator:
10.2 — Confirm service is running
Expected: Status: Running
10.3 — Check tunnel status in dashboard
one.dash.cloudflare.com → Networks → Tunnels → GuyStoreHome
Status must show Healthy (green dot). If it shows Degraded or Inactive, wait 30 seconds and refresh.
10.4 — Test www2 from PowerShell
Expected: 200 https://www2.guydev.ca/
10.5 — Confirm original site still works
Expected: 200 — confirming the IIS-hosted site is unaffected.
Cloudflare dashboard shows GuyStoreHome tunnel as Healthy. https://www2.guydev.ca returns 200. https://guydev.ca still returns 200. The original site is unaffected.
Auto-start on Boot
Two things must start automatically when your PC reboots: Docker Desktop (which also starts the container) and cloudflared (already a Windows service). This section verifies both.
11.1 — Docker Desktop auto-start
Open Docker Desktop → Settings → General. Confirm "Start Docker Desktop when you log in" is checked. Click Apply.
The --restart unless-stopped flag set in §6.2 means Docker will restart the www2 container every time Docker Desktop starts up. No additional configuration needed.
11.2 — Verify cloudflared auto-start
Expected: StartType: Automatic. If it shows Manual, run:
11.3 — Boot order consideration
Docker Desktop starts after Windows login (it needs the WSL 2 subsystem to initialize). There is a ~30–60 second window after login before the container is ready. The cloudflared service starts earlier. This means www2.guydev.ca may return a brief 502 immediately after a reboot — this is normal and self-resolves once Docker is up.
Acceptance Test Checklist
Click each item to mark it complete. All must pass before this runbook is considered done.
- ✓https://www2.guydev.ca loads the GuyStore MVC app in a browser
- ✓https://guydev.ca still loads the original IIS-hosted site (unaffected)
- ✓https://www.guydev.ca still loads (unaffected)
- ✓www2.guydev.ca shows the padlock (HTTPS) — Cloudflare handles TLS
- ✓Tested from mobile hotspot (not home WiFi) — confirms it's public
- ✓Cloudflare dashboard shows GuyStoreHome tunnel as Healthy
- ✓
docker psshows www2 container with status Up - ✓PC rebooted and www2.guydev.ca came back online automatically
Troubleshooting
All issues below were encountered during the actual setup of this runbook and are documented with their exact symptoms, root causes, and fixes.
Issue 1 — wsl --install reports "A distribution with the supplied name already exists"
Running wsl --install returns: A distribution with the supplied name already exists. Use --name to choose a different name.
Root cause: WSL was already installed on the machine from a previous setup. The install command is not needed.
Fix: This is not an error — WSL is already present. Run the following to confirm WSL 2 is active and update the kernel:
Confirm Default Version: 2 is shown. If it shows Version 1, run wsl --set-default-version 2. Then proceed to §3 Docker Desktop.
Issue 2 — curl returns 000 at §7.1 (container not yet started)
curl.exe -s -o NUL -w "%{http_code}" http://localhost:5080/ returns 000.
Root cause: 000 is not an HTTP response code — it means curl could not establish a TCP connection at all. Nothing is listening on port 5080 yet because docker run in §6.2 has not been executed.
Fix: Complete §6 in order before running §7.1. Build the image first, then run the container:
Once docker ps shows www2 with status Up and 0.0.0.0:5080->80/tcp, retry the curl.
Issue 3 — curl still returns 000 after container is Up (SQL Server unreachable)
docker ps shows the container as Up with correct port mapping, but curl still returns 000. docker logs www2 shows: Microsoft.Data.SqlClient.SqlException — A network-related or instance-specific error occurred while establishing a connection to SQL Server. Error 35 — An internal exception was caught.
Root cause: The connection string in appsettings.json uses localhost or . as the SQL Server name. Inside a Docker container, localhost refers to the container itself — not the Windows host where SQL Server lives. The app crashes on startup before it can serve any HTTP requests, so curl gets a connection refused (000).
Fix: Pass the corrected connection string as an environment variable using -e, replacing the server name with host.docker.internal. Docker Desktop maps this special hostname to the Windows host IP automatically.
Replace GuyStoreDb, sa, and YOUR-PASSWORD with the values from your appsettings.json.
Issue 4 — SQL Server TCP/IP not enabled (Error 40)
docker logs www2 shows: SqlException — Could not open a connection to SQL Server. Provider: TCP Provider, Error 40. Note: Error 40 is different from Error 35 in Issue 3 — Error 40 means TCP reached the host but SQL Server refused the connection.
Root cause: SQL Server on Windows defaults to named pipes only. Remote TCP connections (which is what Docker uses via host.docker.internal) are disabled by default and must be explicitly enabled.
Fix — three steps:
Step 1: Open SQL Server Configuration Manager → SQL Server Network Configuration → Protocols for MSSQLSERVER → right-click TCP/IP → Enable → OK.
Step 2: Restart the SQL Server service:
Step 3: Allow SQL Server port through Windows Firewall:
Then stop, remove, and rerun the container as shown in Issue 3.
Issue 5 — SSL certificate rejected by container (Error 35 after TCP enabled)
docker logs www2 shows: A connection was successfully established with the server, but then an error occurred during the pre-login handshake. Provider: TCP Provider, Error 35 — An internal exception was caught. System.Security.Authentication.AuthenticationException: The remote certificate was rejected by the provided RemoteCertificateValidationCallback.
Root cause: After TCP/IP is enabled, the .NET SQL client attempts to validate SQL Server's SSL certificate. Local SQL Server installations use a self-signed certificate which the container's .NET runtime rejects by default. This is the modern Microsoft.Data.SqlClient behaviour — it enforces encryption and certificate validation strictly.
Fix: Add TrustServerCertificate=True to the connection string. This tells the SQL client to skip certificate validation, which is correct and safe for a local development database accessed from a container.
This is the complete working connection string pattern — host.docker.internal as server name plus TrustServerCertificate=True are both required together.
Issue 6 — ingress validate fails in PowerShell ("Unexpected token --config")
Running the cloudflared ingress validate command in PowerShell returns: Unexpected token 'config' in expression or statement. The '--' operator works only on variables or on properties.
Root cause: PowerShell interprets -- as its own operator (end of parameters marker) and also interprets (x86) in the path as a command execution expression. This was the same issue encountered in the original Cloudflare Tunnel runbook session.
Fix: Always run cloudflared commands in CMD (Command Prompt), not PowerShell. Open CMD as Administrator via Win+R → type cmd → press Ctrl+Shift+Enter. Then run:
Expected output: all three hostnames (guydev.ca, www.guydev.ca, www2.guydev.ca) listed with OK.
General — Useful daily management commands
General — www2.guydev.ca returns 502 Bad Gateway
Cloudflare reached cloudflared but cloudflared could not reach the container. Diagnose in this order:
General — www2.guydev.ca returns 404
Cloudflare reached cloudflared but no ingress rule matched the hostname. Verify the config.yml edit was saved and the service restarted. Run in CMD as Admin: