Originally published on the Hubfly space blog.

By Octave · drafted with AI, tested on Hubfly space

TL;DR

  • What you get: one service from a monorepo running on its own HTTPS URL, built from its subfolder, with an optional Dockerfile that lives in the dashboard instead of your repo.
  • Who it's for: teams with apps/api, services/* or packages/* layouts, and anyone whose repo Dockerfile is written for local dev, not production.
  • Time: about 5 minutes of setup. The build itself takes 30–45 seconds for a small Go service.
  • Cost: free on the Shared tier (360 hours a month included), or about $2.23 a month for an always-on 0.5 vCPU / 512 MB Dedicated container.

Your repo holds an API, a web app, a worker and a shared/ folder. Your host wants one app per repo. So you split the repo, or fight the build until it finds the right Dockerfile. Both waste an afternoon.

This guide shows the faster path on Hubfly space: point the build at one folder, replace a laptop-only Dockerfile without touching Git, and handle shared code in a sibling folder. Try it on Hubfly space as you read; the free tier is enough.

The examples use the public docker/awesome-compose repo, which is a real monorepo: dozens of apps, each in its own folder. Swap in your own repo URL and paths.

How the build sees your monorepo

Three settings decide what gets built. The build context is the only set of files a Dockerfile's COPY can see, so getting it right avoids "file not found" errors that look like Docker bugs.

Setting What it does Example
Repository directory The folder the builder treats as the project root. It becomes the build context, and a Dockerfile at its top is used automatically. nginx-golang/backend
Custom Dockerfile Dockerfile text pasted in the dashboard. It replaces any Dockerfile in the repo for this container. A clean two-stage build
Branch Which branch to clone. The form suggests main; change it if your default is master. master

The Source step with a public repository connected, branch master and Repository directory nginx-golang/backend.

Step 1: Create a project and a container

  1. In the dashboard, click Create Project, name it (for example shop), keep Cell (Hubfly space Cell runs managed containers), pick a region and click Create Project.
  2. Click Create Container and name it shop-api. Other services in the project reach it by a private hostname built from this name; the Review step shows the full address.

Step 2: Point the build at one folder

  1. Under Deployment Source, choose Git repository.
  2. For a public repo, click Self-hosted Git and paste its HTTPS URL (<your-repo-url>), for example https://github.com/docker/awesome-compose. No token is needed for public repos. For private GitHub repos, use GitHub App instead; for private GitLab, Bitbucket, Gitea or Forgejo repos, add an access token with read_repository and read_api scopes.
  3. Click Connect Repository.
  4. Set Branch to the branch you deploy (master here).
  5. Set Repository directory to the app's folder (<apps/your-service>): here, nginx-golang/backend.

Leave Automatically detect the build on. If the folder has a Dockerfile at its top, the builder uses it; otherwise it detects the stack from files like go.mod.

Step 3: Pick resources and start the build

  1. On Resources, pick Shared Tier to stay free, or Dedicated Tier for an always-on service. For a small API, 0.5 vCPU and 512 MB is plenty. Untick Auto Sleep if it must answer instantly at all times.
  2. Click Continue through Configuration and Storage (a stateless API needs no volume), then Start Build.

The build page streams every step. In the log, look for Using working directory: nginx-golang/backend. That confirms the build is scoped to your folder.

When the repo's Dockerfile fails: paste one instead

Many monorepo Dockerfiles are written for local dev containers, not production. This example's Dockerfile has a dev-only stage that ends with:

COPY --from=gloursdocker/docker / / 

The build stops with:

Build failed: stage "gloursdocker/docker" not found for COPY --from ERROR: hubcell build failed: exit status 1 

Two things cause this. The builder reads COPY --from= as the name of a stage in the same file, so copying straight from an outside image fails. And it runs the stages in order, so this dev-only stage gets built even though the final image doesn't need it.

The build log ending in: stage "gloursdocker/docker" not found for COPY --from.

You don't need to edit the repo. Use a Custom Dockerfile:

  1. On the failed build page, click Edit & retry. Your settings come back pre-filled.
  2. Untick Automatically detect the build.
  3. Paste this into Custom Dockerfile:
FROM golang:1.22-alpine AS build WORKDIR /src ENV CGO_ENABLED=0 COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o /out/backend main.go FROM alpine:3.20 COPY --from=build /out/backend /usr/local/bin/backend EXPOSE 80 CMD ["/usr/local/bin/backend"] 
  1. Fill Run command with the same command as your CMD: /usr/local/bin/backend. The form requires a run command once auto-detect is off, but with a Dockerfile it's ignored. The log says so: "submitted install/setup/build/run phases are ignored because a Dockerfile was provided."
  2. Continue to Review and click Start Build.

The Custom Dockerfile field filled in, with the cost panel showing $2.23 a month.

Use the Go version your go.mod needs and a current base image tag; the versions above are only an example.

The log now shows Using custom Dockerfile from build request. For this service, the build takes about 41 seconds, and the container is running about a minute after you click Start Build. Why paste instead of commit? You keep the repo's dev setup untouched for your teammates, and the production recipe sits next to the container it builds.

Step 4: Give it a public URL

  1. Open the container and go to Network.
  2. Click Add Domain, keep Auto Endpoint, and check the port. The dialog scans the running container and suggests the open one (80 here).
  3. Click Create Subdomain. You get a generated address like https://<generated-name>.eu1.hubfly.app with TLS on and HTTP redirected to HTTPS.

Open it to see the app. Logs shows the startup line (Go backend started! here).

The app answering on its HTTPS auto endpoint.

When the service needs files from a sibling folder

If backend needs ../shared or a root-level file, setting Repository directory to backend won't work: anything outside that folder is not in the build context.

The fix is to set Repository directory to the closest folder that contains everything, and write paths relative to it:

# Repository directory: nginx-golang FROM golang:1.22-alpine AS build WORKDIR /src ENV CGO_ENABLED=0 COPY backend/go.mod backend/go.sum ./ RUN go mod download COPY backend/ . RUN go build -o /out/backend main.go FROM alpine:3.20 COPY --from=build /out/backend /usr/local/bin/backend COPY README.md /usr/share/doc/shop/README.md EXPOSE 80 CMD ["/usr/local/bin/backend"] 

README.md sits outside the service's own folder and copies fine. This build takes about 32 seconds once base images are cached. Keep the context as narrow as you can: fewer files to send to the builder, and fewer chances that a broad COPY . . pulls something private into the image.

What it costs

Each always-on service here uses 0.5 vCPU, 512 MB of RAM and 4 GB of disk. At Hubfly space's rates (730 hours a month):

  • CPU: 0.5 × $0.004 × 730 = $1.46
  • RAM: 0.5 GB × $0.002 × 730 = $0.73
  • Disk: 4 GB × $0.00001536 × 730 = $0.04
  • Total: about $2.23 a month per service, the same figure the dashboard's cost panel shows. Two services from the same repo: about $4.47.

Builds are free. You pay for the running container. On the Shared tier (0.3 vCPU, 256 MB), a small service like this runs free within the 360 included hours and can sleep when idle.

Limits to know

  • No separate "Dockerfile path" field in the dashboard. The builder uses the Dockerfile at the top of the Repository directory, or your Custom Dockerfile. If your Dockerfile lives elsewhere (say docker/api.Dockerfile), paste its contents into Custom Dockerfile and fix the COPY paths for your chosen directory.
  • COPY --from expects a stage name. A line that copies straight from an outside image fails with stage ... not found. Build what you need in a stage of your own, or leave dev tooling out of the production Dockerfile.
  • Dev-only stages still run. Keep them out of the Dockerfile you deploy, for example by pasting a production-only version into Custom Dockerfile.
  • The Custom Dockerfile lives in the dashboard, not in Git. It isn't code-reviewed or versioned with your repo. For a team, keep a copy in the repo (for example deploy/api.Dockerfile) and paste from there.

What can go wrong

  1. The wrong folder or branch gets built. The Review step doesn't show the Repository directory, and the Branch field suggests main. Check both on the Source step before you click Start Build, especially if your default branch is master or trunk.
  2. stage "..." not found for COPY --from. Your Dockerfile copies from an outside image. Use a Custom Dockerfile without that line.
  3. A COPY line can't find a file. The file is outside the Repository directory. Move the directory up to the common parent and prefix paths, as in the sibling-folder example.
  4. 502 after a successful build. The app is listening on 127.0.0.1 or a different port than the endpoint. Bind to 0.0.0.0 and match the port; see why Docker apps return 502.

Who this is not for

If every service in your repo needs the same shared build (for example a Turborepo turbo prune step that must run from the repo root), you'll write the Custom Dockerfile against the repo root, and build context gets large. That works, but a CI job that builds images and deploys them by image tag may suit you better. On Hubfly space you can redeploy a CI-built image with a deploy webhook.

Takeaway

One field picks the folder, one replaces a Dockerfile that only works locally, and moving up one folder solves shared code. No repo split needed.

Go further on Hubfly space

Start free, 360 hours included. Create your first container on Hubfly space.