DEV Community

Cover image for Why I Deploy to Railway Using GitHub Container Registry Instead of Native Builds
Adam Pitera
Adam Pitera

Posted on

Why I Deploy to Railway Using GitHub Container Registry Instead of Native Builds

TL;DR: Railway's build cache permanently misdetected my Next.js app as Deno, forcing me to delete the entire project. Now I build Docker images locally and deploy via GitHub Container Registry for full control over the build process.

After spending an entire day debugging Railway's build system, I discovered why Docker with GitHub Container Registry (GHCR) can be a better choice than platform-native builds. Here's what I learned and how you can avoid my mistakes.

The Problem That Started Everything

My Next.js app refused to build on Railway. The platform had detected my project as "Deno" (due to a stray test file) and cached this decision permanently. Even after:

  • Removing all Deno files from my repository
  • Adding proper Node.js configuration files
  • Manually deleting Deno from Railway's dashboard (Build > Providers section)
  • Multiple redeploys and cache clears

Railway continued trying to build with Deno. The platform was building phantom files that no longer existed, completely ignoring my current repository state.

The only fix? Delete the entire Railway project and start over.

This experience taught me that "build once, deploy anywhere" with containers beats platform-specific auto-detection when things go wrong.

The GHCR Solution

Instead of relying on Railway's auto-detection, I now build Docker images locally and deploy them via GitHub Container Registry. Here's how:

Prerequisites

  1. Configure Next.js for standalone output in next.config.js:
module.exports = {
  output: 'standalone',  // Essential - creates the server.js file
  // ... your other config
}
Enter fullscreen mode Exit fullscreen mode

2. Set up GitHub authentication (one-time)

# Create a personal access token at github.com/settings/tokens
# with 'write:packages' permission

# Login to GitHub Container Registry
echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
Enter fullscreen mode Exit fullscreen mode

1. Create your Dockerfile

FROM node:18-alpine AS builder
WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci

# Build application
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=3000

# Copy built application
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

2. Build and push to GHCR

# Build your image
docker build -t myapp:latest .

# Tag for GHCR
docker tag myapp:latest ghcr.io/YOUR_GITHUB_USERNAME/myapp:latest

# Push to registry
docker push ghcr.io/YOUR_GITHUB_USERNAME/myapp:latest
Enter fullscreen mode Exit fullscreen mode

3. Configure Railway

Create railway.toml in your repository:

[deploy]
image = "ghcr.io/YOUR_GITHUB_USERNAME/myapp:latest"
Enter fullscreen mode Exit fullscreen mode

Railway now deploys your pre-built image instead of building from source.

Why This Approach Works Better

Control: You define the build process, not the platform. What works locally works in production.

Portability: Your Docker image deploys anywhere—Railway today, AWS tomorrow. No vendor lock-in.

Debugging: Test the exact production image locally with docker run -p 3000:3000 myapp:latest. Build errors surface immediately on your machine.

Predictability: No cached detection states or mystery failures. Docker builds are explicit and reproducible.

Quick Comparison

Railway Native Builds Docker + GHCR
Instant setup Requires Docker setup
Platform controls build You control everything
Limited debugging Test locally first
Railway-only Deploy anywhere

When to Use Each Approach

Railway native builds: Best for standard apps with clean project structures, prototypes, and when you trust the auto-detection to work correctly.

Docker + GHCR: Choose this when you need explicit build control, have hit detection issues, or prefer testing exact production images locally.

The Verdict

Railway's native builds optimise for the happy path—when they work, they're delightfully simple. But when detection goes wrong or you need specific configurations, the magic becomes a liability.

After my debugging marathon, I've learned to value explicit over implicit, control over convenience. Docker with GHCR gives you that control while still deploying to Railway's excellent infrastructure. Your mileage may vary, but for me, the peace of mind is worth the extra setup.

Getting Started

If you're experiencing Railway build issues or want more deployment control:

  1. Start with the Dockerfile above
  2. Adjust for your framework's requirements
  3. Test locally before pushing to GHCR
  4. Enjoy predictable deployments

The extra setup time pays off in deployment confidence and debugging capability.


Useful Resources


Have you switched from platform builds to Docker? What drove your decision? Let me know in the comments.

Top comments (0)