Migrating from Docker Compose to GetWebstack
If your project currently runs with docker compose up, you can move it to GetWebstack without rewriting your services. The two tools describe the same things — services, images, ports, volumes, environment — just with different shapes.
This guide shows the mapping field-by-field, then runs through a worked example.
/gws-setup do most of the workThe fastest migration is to ask an AI agent to do it for you:
/gws-setup migrate this docker-compose.yml to GetWebstack:
generate gws.json + per-service Dockerfiles + manifests, then run gws config import
The skill reads your docker-compose.yml, drafts the equivalent gws.json and Dockerfiles, validates against the live schema, and imports. Use this guide when you want to understand or audit what the agent produced.
How the two models compare
| Concept | Docker Compose | GetWebstack |
|---|---|---|
| Unit of deployment | service in docker-compose.yml | Entry in services[] in gws.json |
| Image | image: or build: | image: or dockerfile: (path to a Dockerfile) |
| Source code | volumes: ["./src:/app"] (bind mount) | fileSync: true (sub-second bidirectional sync) |
| Inbound port | ports: ["3000:3000"] | port: 3000 (Gateway routes traffic) |
| Localhost-only port | ports: ["5432:5432"] | expose: { enabled: true, localPort: 5432 } |
| Environment vars | environment: / env_file: | Manifests (committed) + gws secret (never in gws.json) |
| Depends-on / order | depends_on: | Implicit — every service comes up in its own pod; readiness probes gate traffic |
| Hostname between services | service name (db) | service name resolves inside the namespace |
| Override per environment | multiple compose.yml files | Profiles inside one gws.json |
| Stateful storage | volumes: ["pgdata:/var/lib/postgresql/data"] | manifests/ folder with a PersistentVolumeClaim |
| Managed services (Postgres, Redis, …) | image: postgres:16 | Same — or a Helm service |
Two things to note:
- No more
depends_on. GetWebstack runs every service in its own pod and uses health/readiness probes. The Gateway only routes once a service answers; nothing else needs to wait by hand. - Secrets stop living in your config. What was
environment: { DB_PASSWORD: foo }becomes a Kubernetes Secret applied via themanifests/folder, plusgws secret set DB_PASSWORDfor values you don't want in git.
Worked example
A small Compose project — frontend, API, Postgres, Redis worker:
# docker-compose.yml
version: "3.9"
services:
web:
build: ./frontend
ports: ["80:80"]
volumes: ["./frontend:/app"]
depends_on: [api]
api:
build: ./api
ports: ["3000:3000"]
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
REDIS_URL: redis://cache:6379
volumes: ["./api:/app"]
depends_on: [db, cache]
worker:
build:
context: ./api
dockerfile: Dockerfile.worker
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
REDIS_URL: redis://cache:6379
depends_on: [db, cache]
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes: ["pgdata:/var/lib/postgresql/data"]
cache:
image: redis:7
volumes:
pgdata:
The equivalent gws.json:
{
"name": "my-project",
"version": "1.0.0",
"services": [
{
"name": "web",
"path": "./frontend",
"port": 80,
"subdomain": "app",
"fileSync": true,
"fileSyncIgnore": ["node_modules/", "dist/"]
},
{
"name": "api",
"path": "./api",
"port": 3000,
"subdomain": "api",
"fileSync": true,
"fileSyncIgnore": ["node_modules/", "dist/"]
},
{
"name": "worker",
"path": "./api",
"dockerfile": "./api/Dockerfile.worker",
"fileSync": true
},
{
"name": "db",
"path": "./db",
"port": 5432,
"expose": { "enabled": true, "localPort": 5432 },
"helm": {
"chart": "bitnami/postgresql",
"version": "15.5.10",
"repositories": [
{ "name": "bitnami", "url": "https://charts.bitnami.com/bitnami" }
],
"values": {
"auth": {
"username": "app",
"database": "app",
"password": "${POSTGRES_PASSWORD}"
}
}
}
},
{
"name": "cache",
"path": "./cache",
"port": 6379,
"expose": { "enabled": true, "localPort": 6379 },
"helm": {
"chart": "bitnami/redis",
"version": "20.0.4",
"repositories": [
{ "name": "bitnami", "url": "https://charts.bitnami.com/bitnami" }
],
"values": { "architecture": "standalone", "auth": { "enabled": false } }
}
}
]
}
What changed:
build:paths →path:+ (optional)dockerfile:ports: ["80:80"]→port: 80for routed servicesports: ["5432:5432"](DB exposed forpsql) →expose: { enabled: true, localPort: 5432 }volumes: ["./api:/app"](bind mount) →fileSync: truewithfileSyncIgnoredepends_on:— gone, replaced by readiness probes in each service'smanifests/folder- Hardcoded passwords moved out: see Secrets below
- Postgres + Redis switched to Helm services so you don't maintain the manifests yourself
Secrets
Anything that was environment: { PASSWORD: ... } should leave gws.json and become a gws secret:
# Interactive prompt (recommended — no shell history)
gws secret set POSTGRES_PASSWORD --service db
gws secret set DATABASE_URL --service api
gws secret set REDIS_URL --service api
# Or read from a file in CI
gws secret set POSTGRES_PASSWORD --service db --stdin < /run/secrets/db_password
Reference them from your manifests/ envFrom block (Kubernetes injects them as env vars at deploy time). They never appear in git.
Per-environment overrides
What you used to do with docker-compose.override.yml or docker-compose.prod.yml belongs in a profile:
"profiles": {
"ci": { "services": [{ "name": "*", "fileSync": false }] },
"e2e": {
"services": [
{ "name": "*", "fileSync": false, "watch": null },
{ "name": "api", "dockerfile": "./api/Dockerfile.e2e" }
]
}
}
Then gws up --profile e2e instead of docker compose -f docker-compose.yml -f docker-compose.e2e.yml up.
Migration checklist
- Inventory: list every service in
docker-compose.yml, plus its image/build, ports, volumes, env, and depends-on. - Generate the initial
gws.json— either via/gws-setupor by hand using the table above. - Replace bind mounts with
fileSync: trueand add afileSyncIgnorefor build artifacts. - Move secrets out of
environment:intogws secret setandenvFromin your manifests. - Swap stateful services (Postgres, MySQL, Redis, Mongo, …) to Helm services.
- Drop
depends_on— write proper readiness probes inmanifests/instead. - Map override files to profiles.
- Validate:
gws config validatethengws config import. - Run it:
gws up. Opengws statusand confirm every service is healthy. - Compare: keep
docker-compose.ymlaround for one or two PRs while you confirm parity, then remove it.
See also
- Zero-Config Setup with AI — how
/gws-setupworks under the hood gws.jsonoverview and Services- Profiles — the replacement for
docker-compose.override.yml gws secret— how to handle env vars that shouldn't be in git