Building apps
Dockerfile apps
Most apps fit the Node buildpack just fine. But when you need a different language, an unusual
runtime, or full control over how the app is built, you can hand Astrodock a Dockerfile
instead. The platform builds your image and runs it as a container, and the whole subdomain is
yours.
When to use a Dockerfile
- Non-Node runtimes — Python, Go, Ruby, anything that isn't Node.
- Full control over system packages, build steps, or the process model.
- Existing containerized apps you'd rather not rewrite to fit the buildpack.
If your app is a normal Node frontend + server, you almost certainly want the buildpack instead — see App structure & app.json. Use Docker when the buildpack can't express what you need.
Switch the runtime to docker
One field flips an app from the Node buildpack to the container path:
"runtime": {
"type": "docker",
"dockerfile": "Dockerfile"
}
runtime.dockerfile is optional — it defaults to Dockerfile at the deploy
root. Set it only if your Dockerfile has a different name or lives elsewhere (relative to
source.repoPath).
The container contract
Whatever's inside your container, two rules from the buildpack still apply — the platform uses them to route traffic and to know your app is alive:
-
Listen on
ASTRODOCK_PORTThe platform injects
ASTRODOCK_PORT(and aPORTalias) into the container and proxies the subdomain to it. Your process must bind that port — don't hardcode one. -
Answer
GET /healthwith a 200After the container starts, the platform probes
/health. A 200 marks the deploy healthy; without it the deploy is recorded as failed.
Be the foreground process
Your container's main process must be the server itself (so the platform can supervise and
restart it). If the container exits or the app is unreachable, double-check it's listening on
ASTRODOCK_PORT and running in the foreground.
A minimal example
A tiny Python web service. It reads the injected port and serves both /health and the
app on it:
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# The platform injects ASTRODOCK_PORT (and PORT) — bind it, don't hardcode.
CMD ["python", "main.py"]
# main.py (Flask)
import os
from flask import Flask
app = Flask(__name__)
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/")
def home():
return f"Hello from {os.environ['ASTRODOCK_APP_NAME']}"
if __name__ == "__main__":
port = int(os.environ.get("ASTRODOCK_PORT", os.environ.get("PORT", 8000)))
app.run(host="0.0.0.0", port=port)
And the matching app.json:
{
"schemaVersion": "1",
"slug": "py-tool",
"name": "Python Tool",
"subdomain": "pytool",
"source": { "branch": "main", "repoPath": "service" },
"runtime": { "type": "docker", "dockerfile": "Dockerfile" },
"auth": { "mode": "platform" },
"database": { "mode": "internal" },
"storage": { "mode": "none" },
"env": [
{ "key": "WORKERS", "required": false, "default": "2",
"description": "Number of worker processes" }
]
}
Note repoPath: "service" — the build happens in that subdirectory, so the Dockerfile
and code live under service/ in the repo.
How environment is injected
Every variable a buildpack app would get, a Docker app gets too — they're passed into the container as environment variables when it runs. That means inside your container you can read:
- The always-present vars:
ASTRODOCK_APP_SLUG,ASTRODOCK_APP_NAME,ASTRODOCK_APP_URL,ASTRODOCK_BASE_DOMAIN,ASTRODOCK_PORT,ASTRODOCK_ENV(plus thePORTalias). - Resource and auth vars, depending on your modes (e.g.
ASTRODOCK_DATABASE_URLwhendatabaseisn'tnone). - Your own declared
env[]variables.
Read them from the environment exactly as you would anywhere else — the full catalog is in the app.json & env reference. Don't bake secrets into the image; let the platform inject them at run time.
Resource modes still apply
Choosing docker changes only how the app is built and run — not what resources it can
use. auth, database, and storage work exactly the same as for a
Node app: pick internal, external, or none, and the matching
variables are injected. An external mode still triggers the
required-variable gate before the build runs.
Differences from the Node buildpack
| Node buildpack | Dockerfile | |
|---|---|---|
| Layout | app/ + server/ (or a standalone variant) | No split — your container serves everything |
| Routing | Static frontend at the URL; /api/* proxied to the server | The whole subdomain is proxied to your container |
| Static files | Caddy serves your built app/dist/ | Your container serves its own assets |
| Build | npm ci + buildCommand / npm install | docker build from your Dockerfile |
| Run | A Node process | A container |
No /api requirement for Docker apps
Because the whole subdomain goes to your container, you're free to route however you like — the
/api/* namespacing is a Node-buildpack rule, not a Docker one. You still must bind
ASTRODOCK_PORT and answer /health.
Next steps
- Deploy it: The deploy lifecycle
- The other shapes: App structure & app.json
- The full variable catalog: app.json & env reference