Files for projects that opted into Docker during atom-setup. The
generic preset targets Node-based projects. For stack-specific
optimizations (Next.js, Python, Go, etc.), see the matching
extras/<category>/<preset>/.
| File | Purpose |
|---|---|
Dockerfile |
Generic Node-based, multi-stage, production-grade. |
.dockerignore |
Standard exclusions for the build context. |
docker-compose.yml |
Backing services only (Postgres, Redis). Default for macOS dev. |
docker-compose.full.yml |
Everything containerized. For CI, Linux dev, full parity testing. |
.devcontainer/devcontainer.json |
VSCode dev container config. Uses docker-compose.full.yml. |
.github/workflows/docker.yml |
Multi-arch (amd64 + arm64) build + push to GHCR. |
atom-setup copies a subset based on the user's choice in §6 of the
wizard:
| Tier | Files copied |
|---|---|
| None | (none) |
| Dockerfile only | Dockerfile, .dockerignore, .github/workflows/docker.yml |
| + compose | Above + docker-compose.yml, docker-compose.full.yml |
| + devcontainer | Above + .devcontainer/ |
The CI workflow (docker.yml) ships whenever the Dockerfile does.
For the None tier, no Docker files land in the project at all.
When a stack preset (extras/<category>/<preset>/) ships its own
Dockerfile, atom-setup uses that instead of this generic one. The
stack preset's Dockerfile is opinionated and tuned for that stack
(Next.js standalone output, Python uv lockfiles, Go binary
distroless runtime, etc.).
Currently shipped per-stack Dockerfiles:
| Preset | Dockerfile location |
|---|---|
| Next.js + Railway | extras/web/nextjs-railway/Dockerfile |
Add new stack-specific Dockerfiles by dropping a Dockerfile (and
matching .dockerignore) into the preset's directory. atom-setup
auto-discovers them.
The shipped Dockerfile assumes Node. To switch:
Replace the base image and dep install:
ARG PYTHON_VERSION=3.12-slim
FROM python:${PYTHON_VERSION} AS deps
WORKDIR /app
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
pip install --no-cache-dir -r requirements.txt
FROM python:${PYTHON_VERSION} AS runtime
WORKDIR /app
RUN addgroup --system --gid 1001 app \
&& adduser --system --uid 1001 --gid 1001 app
COPY --from=deps /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --chown=app:app . .
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz').read()" || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]If using uv: replace pip install with uv sync --frozen.
Multi-stage with a distroless runtime:
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/bin/server ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot AS runtime
COPY --from=builder /app/bin/server /server
USER nonroot:nonroot
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD ["/server", "healthcheck"]
ENTRYPOINT ["/server"]FROM rust:1.82-alpine AS builder
WORKDIR /app
RUN apk add --no-cache musl-dev
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release && cp target/release/server /server
FROM alpine:3.20 AS runtime
RUN addgroup --system app && adduser --system -G app app
COPY --from=builder /server /usr/local/bin/server
USER app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/healthz || exit 1
CMD ["server"]These are non-negotiable across every Dockerfile in atom:
- Multi-stage build — builder has dev deps; runtime has only what runs.
- Non-root user —
USER appuser(uid 1001). - Healthcheck — every Dockerfile defines
HEALTHCHECKagainst/healthz(or stack equivalent). The route returns 200 when ready, 503 when degraded. Required env missing = 503; optional env missing = 200 with adegradedflag. - Pinned base image —
node:22.11-alpine, notnode:alpine. Renovate or Dependabot bumps majors via PR. - BuildKit cache mounts —
--mount=type=cachefor package manager caches. Enable withDOCKER_BUILDKIT=1. .dockerignorealongside, excluding.git,node_modules,.env*,dist, etc.- Multi-arch CI —
.github/workflows/docker.ymlbuildsamd64andarm64viadocker buildx. Doubles CI minutes; worth it for OSS but watch the bill on paid CI.
Default docker-compose.yml is backing services only. Run the app
natively (npm run dev) on macOS for fast hot reload:
docker compose up -d # postgres + redis only
npm run dev # nativeFor everything in containers (CI, Linux, parity testing):
docker compose -f docker-compose.full.yml upThe full mode bind-mounts source for live reload but is 2-3x slower on macOS due to Docker Desktop file-system overhead.
Every Dockerfile shipped expects /healthz (or /api/health for
Next.js per Vercel convention). Your app must implement this route:
- 200 OK — required deps present, app ready.
- 200 OK +
degraded: trueJSON — optional deps missing, app partially functional. - 503 Service Unavailable — required deps missing or app failed to start.
Without this route, the container reports unhealthy. Compose healthchecks loop forever.