# Deploying the stack to a Windows VM > **Where this sits:** a rehearsal, not a site. For a real site follow > [DEPLOY](../DEPLOY.md). To only LOOK at a build with no source at all, > [DEV-BUILD-TEST](../DEV-BUILD-TEST.md) pulls a published image and is one > script. A Windows VM with Docker Desktop is the cheapest way to prove a deploy before doing it for real: the stack is identical to a site's, the VM can be reverted, and nothing on it matters. This page is the VM-specific part - getting the code there, the firewall, reaching it from the host. Everything else is [DEPLOY](../DEPLOY.md). ## On the VM 1. **Docker Desktop**, installed and running. 2. **Git Bash or WSL**, for the bash commands. 3. Enough RAM given to Docker. The stack is MySQL plus a Python container plus nginx; 8GB to Docker is comfortable. ## Step 1: Get the code onto the VM ```powershell # From a network share Copy-Item -Recurse \\\shopdb-flask C:\shopdb # Or clone it git clone /shopdb-flask.git C:\shopdb # Or hand it over as a zip, which is what an isolated VM usually wants Expand-Archive -Path C:\shopdb-deploy.zip -DestinationPath C:\shopdb ``` ## Step 2: Bring it up The pre-built path is the one to use on a VM: npm inside the image is slow and is the step most likely to fail behind a proxy. Build the frontend once on the host or on the VM, then build the image from it - [DOCKER-PREBUILT-APPROACH](DOCKER-PREBUILT-APPROACH.md). ```powershell cd C:\shopdb copy .env.example .env # Edit .env: SECRET_KEY, JWT_SECRET_KEY, MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD, # CORS_ORIGINS. Generate a secret with: # python -c "import secrets; print(secrets.token_urlsafe(64))" cd frontend; npm ci; npm run build; cd .. docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml --profile backup build docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml --profile backup up -d ``` The `migrate` service runs the schema, the plugin bootstrap and the seeders before `api` starts, so there is no separate migration step. Watch it with `docker compose logs migrate`. Create the first admin at `https://localhost:5001/setup`, or headlessly: ```powershell docker compose exec api flask seed admin --username admin --email admin@example.com ``` The password is generated and printed once. ## Step 3: Reach it from the host ```powershell New-NetFirewallRule -DisplayName "ShopDB" -Direction Inbound -LocalPort 5001 -Protocol TCP -Action Allow ``` Then `https://:5001` from the host. The certificate is self-signed until you replace the pair in `certs/`, so the browser warns; that is expected on a VM. `CORS_ORIGINS` is an exact origin match, so it has to name the address the browser really uses. Reaching the VM by IP while `CORS_ORIGINS` names `localhost` is the usual cause of "the page loads and every request fails". ## About `deploy-to-vm.ps1` The repository root carries `deploy-to-vm.ps1`, which wraps the above into eight steps with `-SiteName` and `-AdminPassword` parameters. **Read it before you run it.** As it stands it does not work on a clean clone, and neither problem is subtle: - It copies `.env.docker-dev` to create `.env`. That file is not in the repository, so step 3 fails on a fresh checkout. Create `.env` from `.env.example` yourself first and the script skips that branch. - It addresses containers by the fixed names `flask-shopdb-api-1` and `flask-shopdb-db-1`. Compose names containers after the project, which is the directory name, so on a clone called `shopdb` those `docker exec` calls miss. Use `docker compose exec api` instead, or `docker compose ps` to see the real names. It also runs only `flask db upgrade` for the database, which predates the `migrate` service and is now redundant rather than wrong. Its default `-AdminPassword` is a literal in the file and therefore public. Pass your own, and never leave the default on anything reachable. ## Stop, start, update ```bash docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml down docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml --profile backup up -d ``` `down` keeps the volumes. `down -v` deletes them, which is the database and `instance/` together, with no prompt. To take new code: ```bash git pull cd frontend; npm run build; cd .. docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml build docker compose -f docker-compose.prebuilt.yml -f docker-compose.backup.yml up -d ``` Rebuilding the frontend is not optional on the pre-built path: the image copies whatever is in `frontend/dist/`, so a skipped `npm run build` ships the old UI with no warning. ## Why this works with no network With `frontend/dist/` already built and the Python wheels vendored, the image build downloads nothing, which is what makes the same procedure the rehearsal for [DEPLOY-AIRGAP](../DEPLOY-AIRGAP.md).