Deploying with Docker Compose
Learn how to self-host your Hackhaton-Space-Stack app with the generated Dockerfiles and docker-compose.yml
Overview
This guide explains how Hackhaton-Space-Stack packages your applications for self-hosting with Docker Compose. You'll learn:
- What gets generated and how the compose stack is wired
- How to build, run, and monitor the containers
- How environment variables flow between builds, containers, and the compose file
- How the optional Docker database service works
- What to change when moving from local containers to a real host
What Gets Generated
Choosing docker as a deploy target generates:
docker-compose.ymlat the repo root defines theweb,server, and (optionally) database services with health checks and startup ordering.apps/*/Dockerfileare multi-stage builds that install the whole monorepo workspace, build one app, and ship a minimal runtime image.nginx.confis generated for the static TanStack Router SPA, which is served by nginx. Solid runs as a Node SSR server from its Nitro.outputbuild.
Enabling Docker Deployment
Combined deployment (web + server + database):
npm create hackhaton-space-stack@latest my-app \
--frontend tanstack-router \
--backend hono \
--runtime bun \
--database postgres \
--db-setup docker \
--web-deploy docker \
--server-deploy dockerWeb-only (fullstack self backend):
npm create hackhaton-space-stack@latest my-app \
--frontend next \
--backend self \
--web-deploy dockerServer-only:
npm create hackhaton-space-stack@latest my-app \
--frontend none \
--backend fastify \
--runtime node \
--server-deploy dockerServer deployment requires the bun or node runtime. Adding --db-setup docker puts a Postgres, MySQL, or MongoDB container in the same compose file with a named volume and health check.
Running the Stack
bun docker:build # docker compose build
bun docker:up # docker compose up -d --build
bun docker:logs # docker compose logs -f
bun docker:down # docker compose downAfter docker:up:
| Service | URL | Notes |
|---|---|---|
| web | http://localhost:3001 | nginx (SPA frontends) or the framework's server |
| server | http://localhost:3000 | health-checked; web waits for it to become healthy |
| database | localhost:5432 / 3306 / 27017 | only with --db-setup docker |
Startup ordering is handled with depends_on + condition: service_healthy: the database must pass its health check before the server starts, and the server must pass its fetch('http://localhost:3000/') health check before the web container starts.
With --db-setup docker you also get scoped database scripts for local development without the full stack:
bun db:start # docker compose up -d postgres (just the database, detached)
bun db:watch # docker compose up postgres (with logs in the foreground)How the Images Build
Each Dockerfile is a multi-stage build with the monorepo root as build context:
- A base stage installs the workspace with your package manager (with a mounted dependency cache, so rebuilds are fast).
- Compose supplies the generated app env files as BuildKit secrets during installation and builds. Varlock validates the app schema, including database configuration needed by Prisma tooling. Public web variables also use build arguments. Fill required configuration before building; validation is not bypassed.
- Runtime images retain the built output and the schema/Varlock CLI dependencies needed for startup. Next standalone preloads
varlock/auto-loadwith Node. SPA frontends use nginx; SSR frontends and servers use Bun or Node images.
Environment Variables
Three rules cover everything:
- Public web variables are baked at build time. Values like
VITE_SERVER_URLare compose build args — they're inlined into the client bundle when the image builds. The generated default ishttp://localhost:3000(the published server port). Changing them requires an image rebuild, not just a restart. - Runtime variables come from each app's
.envfile. Compose loads app values throughenv_file. The same files are required as build-secret sources when building images; Varlock rejects missing required values. - Compose overrides handle container networking. Inside the Docker network, containers reach each other by service name, not
localhost— so compose sets values likeDATABASE_URL: postgresql://postgres:...@postgres:5432/my-appandCORS_ORIGIN: http://localhost:3001in theenvironment:block, which wins overenv_file. Your local.envfiles stay pointed at localhost for non-Docker development.
BuildKit keeps the mounted env files out of normal COPY layers. Frameworks that embed resolved server configuration, including Next, can still put those values in server build artifacts. See Varlock encrypted deployments for encrypting that configuration with a key supplied at both build and runtime.
Database passwords default to password and can be overridden with compose variables (POSTGRES_PASSWORD, MYSQL_PASSWORD, MONGO_PASSWORD) — set them in a root .env file or export them before docker:up.
Moving to a Real Host
The generated compose file is production-shaped (multi-stage images, health checks, restart: unless-stopped, named volumes), but a few things are sized for localhost:
- Put a reverse proxy in front. Terminate TLS with Caddy, Traefik, or nginx and route your domain to the web container; stop publishing the server and database ports publicly once the proxy handles routing.
- Update baked URLs for your domain. Rebuild the web image with the public server URL build arg set to your real API origin, and set
CORS_ORIGIN/BETTER_AUTH_URLon the server to your real web origin. - Set real secrets. Replace the default database password and put production values in the server's environment (compose
environment:, an.envfile on the host, or your orchestrator's secret store). - Back up the database volume. Data lives in a named Docker volume (
<project>_postgres_dataetc.);docker compose down -vdeletes it.
Troubleshooting
Port already in use
The stack publishes 3001 (web), 3000 (server), and the database port. Stop whatever is bound to them, or edit the ports: mappings in docker-compose.yml.
Changed a public env var but the web app didn't pick it up
Public variables are build args baked into the client bundle. Rebuild the image: bun docker:up (it passes --build) or bun docker:build first.
Server can't reach the database with a localhost URL
Inside compose, the database host is the service name (postgres, mysql, mongodb), not localhost. Use the compose-provided DATABASE_URL (already set in the environment: block) instead of the one in apps/server/.env.
Web container never starts
It waits for the server health check. Check bun docker:logs — if the server is crash-looping (usually missing env vars or a bad DATABASE_URL), the web container stays in waiting state.
CORS errors in the browser
The compose file sets CORS_ORIGIN: http://localhost:3001 when the web service is present. If you access the app from a different host or port, update that value to match the origin you're actually using and restart.