Project 2 > Cloud Deployment
Video Walkthroughs
1. Update Frontend API Configuration
Before creating the production Dockerfile, you need to update your App.jsx file to use an environment variable for the API URL. This allows your app to work in both development (localhost) and production (Railway).
Open ui/src/App.jsx and update the API URL configuration at the top of the file:
// Use environment variable for API URL, default to localhost for development
// In production (Railway), this will be empty string since frontend and backend are same origin
// Check if VITE_API_URL is explicitly set (even if empty string), otherwise use localhost
const API_URL =
import.meta.env.VITE_API_URL !== undefined
? import.meta.env.VITE_API_URL // address for production architecture
: 'http://localhost:8000'; // address for local architecture
What this does:
- In development,
VITE_API_URLis not set, so it defaults tohttp://localhost:8000 - In production, the Dockerfile will set
VITE_API_URLto an empty string (since frontend and backend are served from the same origin) - When
VITE_API_URLis empty, your API calls will use relative URLs (e.g.,/api/todosinstead ofhttp://localhost:8000/api/todos)
Important: Make sure you’re using API_URL in all your fetch() calls throughout your React components. For example:
fetch(`${API_URL}/api/todos`)
Before you move on
Verify that:
- Your
App.jsx(or wherever you define your API URL) uses the environment variable- All your
fetch()calls use theAPI_URLconstant instead of hardcoded URLs
2. Create a Production Dockerfile
Create Dockerfile.prod in the root directory:
# Combined Production Dockerfile for Railway
# Builds both frontend and backend, serves them together
# Stage 1: Build Frontend
FROM node:20-slim as frontend-builder
WORKDIR /app/frontend
# Accept VITE_API_URL as build argument (defaults to empty string for production)
ARG VITE_API_URL=""
ENV VITE_API_URL=$VITE_API_URL
# Copy frontend package files
COPY ui/package.json ./
# Install dependencies
RUN npm install --production=false && \
npm install @rollup/rollup-linux-x64-gnu --save-optional || true
# Copy frontend source code
COPY ui/ ./
# Build the React app
RUN npm run build
# Stage 2: Build Backend Dependencies
FROM python:3.11-slim as backend-builder
WORKDIR /app
# Install Poetry
RUN pip install --no-cache-dir poetry
# Configure Poetry
RUN poetry config virtualenvs.create false
# Copy backend dependency files
COPY backend/pyproject.toml backend/poetry.lock* ./
# Install dependencies (production only)
RUN poetry install --no-interaction --no-ansi --only=main --no-root
# Stage 3: Final Runtime Image
FROM python:3.11-slim
WORKDIR /app
# Copy installed Python packages and binaries from builder
COPY --from=backend-builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=backend-builder /usr/local/bin /usr/local/bin
# Copy backend application code
COPY backend/ ./backend/
# Copy built frontend from frontend-builder to /app/static
COPY --from=frontend-builder /app/frontend/dist ./static
# Create non-root user for security
RUN useradd -m -u 1000 appuser && \
chown -R appuser:appuser /app
USER appuser
# Expose port (Railway sets PORT automatically)
EXPOSE 8080
# Use PORT environment variable if set, otherwise default to 8080
ENV PORT=8080
# Change to backend directory and run server
WORKDIR /app/backend
CMD sh -c "uvicorn server:app --host 0.0.0.0 --port \${PORT:-8080}"
What this Dockerfile does:
This Dockerfile uses a multi-stage build to create a single container that runs both your frontend and backend together. Here’s what happens in simple terms:
Stage 1: Build the Frontend
- Starts with a Node.js image
- Installs all the frontend dependencies (React, Vite, etc.)
- Builds your React app into static files (HTML, CSS, JavaScript)
- The built files are saved in a
distfolder
Stage 2: Prepare the Backend
- Starts with a Python image
- Installs Poetry and all your Python dependencies (FastAPI, SQLAlchemy, etc.)
- Gets everything ready to run your backend
Stage 3: Combine Everything
- Takes the built frontend files from Stage 1 and copies them into the final container
- Takes the backend code and Python packages from Stage 2 and copies them into the final container
- Creates a non-root user for security
- Sets up the container to run your FastAPI server, which will serve both:
- Your backend API (at
/api/*endpoints) - Your frontend static files (at the root
/)
- Your backend API (at
The result: One container that has everything your app needs to run in production. When Railway starts this container, it runs your FastAPI server, which handles both API requests and serves your React frontend.
Important Notes:
- This is a multi-stage build that combines frontend and backend
- The frontend is built and served as static files by the backend
- Make sure your
server.pyhas code to serve static files in production
Before you move on
Verify your production Dockerfile is in the root directory.
3. Sign up for Railway
- Go to https://railway.app
- Sign up with your GitHub account (recommended) or email
- Railway offers a free tier with $5/month credit
4. Create a new project
- Click “New Project” in the Railway dashboard
- Choose “Empty Project (the bottom option)
Within your new project, you will add two new containers (called services)
5. Create a PostgreSQL service
- From within your project, click the “Add a service” button
- Select “Database” → “Add PostgreSQL”
- Railway will automatically create a PostgreSQL database
Once the service has been built, click on the PostgreSQL service you just created. Then:
- Go to the “Variables” tab
- Find the
DATABASE_URLvariable (Railway creates this automatically) - Copy the connection string and paste it somewhere safe:
postgresql://postgres:password@hostname.railway.app:5432/railway - Modify the connection string to work with asynchronous connections:
- Railway’s
DATABASE_URLuses the format:postgresql://... - SQLAlchemy with asyncpg needs:
postgresql+asyncpg://... - Replace
postgresql://withpostgresql+asyncpg://in your connection string
- Railway’s
Before you move on
Verify you have:
- A PostgreSQL database created in Railway
- The
DATABASE_URLconnection string copied- The connection string modified to use
postgresql+asyncpg://
6. Create a Web Service
Now that you’ve created your database container, you’re ready to create your web server, which will host your Backend and your Frontend on the same box. To do this, you will add a second service to your project:
- Add your application service:
- In your Railway project dashboard, click “+ New”
- Select “GitHub Repo” → choose your repository
- You will also need to teach Railway how to deploy your web server by going to Settings → Deploy → Dockerfile Path:
Dockerfile.prod
- Set environment variables:
- Go to the “Variables” tab in your application service
- Add the
DATABASE_URLvariable:- Click “+ New Variable”
- Name:
DATABASE_URL - Value: Your modified connection string (with
postgresql+asyncpg://) - Click “Add”
- Important: Use the modified connection string (with
+asyncpg) here
- Deploy:
- Railway will automatically deploy when you push to your main branch
- Or click “Deploy” in the dashboard to trigger a manual deployment
- Watch the build logs to see the deployment progress
- Get your application URL:
- Once deployed, Railway will provide a public URL
- Go to Settings → Networking → Generate Domain
- Your app will be available at:
https://your-app-name.up.railway.app
Before you move on
Verify your application is:
- Deployed on Railway
- Connected to the PostgreSQL database
- Accessible via a public URL
- Test it by visiting your Railway URL
7. Verify Deployment
Test your deployed application:
- Visit your Railway URL:
- Open
https://your-app-name.up.railway.appin your browser - Your application should load
- Open
- Test API endpoints:
- Visit
https://your-app-name.up.railway.app/docsto see the FastAPI documentation - Test the
/todosendpoint to verify database connectivity
- Visit
- Test CRUD operations:
- Create a new todo
- Read todos
- Update a todo
- Delete a todo
- Verify all operations work correctly
Before you move on
Verify your deployed application:
- Loads correctly in the browser
- Has working API endpoints
- Successfully performs all CRUD operations
- Is accessible from any device with internet access